打算開始打理個人公衆號了,最新內容和獨家祕籍。走過路過來捧個場,關注公衆號:編程之王
java
本文收錄於公衆號
編程之王
: 文章內存地址sjms-o-05
如何獲取更多知識乾糧
,詳見<<編程之王食用規範1.0>>
android
單例模式:
保證一個類僅有一個實例,並提供一個訪問他的全局訪問點
編程
單例模式像一個奇葩,和設計原則格格不入。如今對象的建立,所以無接口拓展可言
依賴倒置原則
、接口隔離原則
、迪米特原則
、里氏替換原則
、合成複用原則
研究無從談起。
單例沒法派生本身的族系,全部修改都要在本體中進行,違反開放封閉原則
單例是典型的大包乾,功能的集聚地,可能存在職責太重,而違反單一職責原則
設計模式
設計原則旨在協調
一個軟件實體(類、模塊、函數)
之間的結構關係 。
而單例每每只是一個類,沒有本身的族系和朋友圈,它就像孤獨而至高的王
。
其次是由於它真的很是簡單和好用。沒有抽象的族系拓展,讓它能夠很容易被理解。安全
---->[優點]----
[1].全局內存中只需有一個實例對象,減少內存開銷
[2].使用一個對象提供訪問,避免對稀缺資源的多重佔用
[3].私有化構造,提供全局的惟一訪問點,嚴格控制訪問
---->[劣勢]----
[1].無接口拓展可言,全部修改都要在本體中進行
[2].可能存在職責太重,而違反單一職責原則
複製代碼
若是上線一個世界程序,
一個World對象佔據內存10G
世界不能隨便去new,如何不讓上層沒法主動建立World對象,
World對象佔據內存太大,服務器沒法支撐多個世界對象,須要提供惟一World對象bash
關於單例的幾個要點:
[1].私有構造:將類的構造私有化,從而限制外界訪問。
[2].延遲加載:當且僅當第一次獲取單例對象是纔會建立對象。
[3].線程安全:多線程時不會建立多個該類對象。
[4].防反序列化:反序列化不會建立多個該類對象。
[5].防反射:反射不會建立多個該類對象。
複製代碼
形式上的一切都僅是開始而已
極簡單例(餓漢)
做爲靜態變量直接建立,最大的缺點是單例對象爲沒有延遲加載性服務器
public class World {
private final static World sWorld = new World();
//[1]私有化構造
private World() {
initWorld();//初始化世界
System.out.println("世界已建立");
}
private void initWorld() {
}
//[2]返回內部靜態實例
public static World getInstance() {
return sWorld;
}
}
複製代碼
單線程懶加載(懶漢)
最大的缺點是線程不安全,怎麼個不安全法,且聽我細細道來。微信
public class World {
private static World sWorld = null;
//[1]私有化構造
private World() {
initWorld();//初始化世界
System.out.println("世界已建立");
}
private void initWorld() {
}
//[2]返回內部靜態實例
public static World getInstance() {
if (sWorld==null){
sWorld=new World();
}
return sWorld;
}
}
複製代碼
之因此稱爲單例,是由於在屢次調用getInstance獲取實例時是相同實例,且構造只執行一次網絡
public class Client {
public static void main(String[] args) {
World world = World.getInstance();
World world2 = World.getInstance();
World world3 = World.getInstance();
System.out.println(world);//World@41cf53f9
System.out.println(world2);//World@41cf53f9
System.out.println(world3);//World@41cf53f9
}
}
複製代碼
之因此說線程不安全,由於多線程下
sWorld==null
可能被屢次經過,因此實例化多個對象。
演示一下,在一個Machine的Runnable對象中調用了World.getInstance()
來獲取World對象多線程
public class Machine implements Runnable {
public void run() {
World.getInstance();
}
}
複製代碼
這時在Client中建立1000個線程去使用這個World,千人同時在線,每一個用戶一個訪問線程
若是不做線程安全處理,就會建立多個世界,若是一個世界的渲染須要10G內存,結果可想而知,這樣單例就沒有意義了。
public class Client {
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(new Machine()).start();
}
}
}
複製代碼
若是你會多線程調試,能夠本身干預一下線程的執行。
第一檢--該對象是否非空,
爲空才進行同步鎖定
第二檢--該對象是否非空,爲空才建立實例
public class World {
private volatile static World sWorld;
//[1]私有化構造
private World() {
initWorld();//初始化世界
System.out.println("世界已建立");
}
private void initWorld() {
}
//[2]返回內部靜態實例
public static World getInstance() {
if (sWorld == null) {//判斷非空後--執行
synchronized (World.class) {//加鎖,保證多線程下的單例
if (sWorld == null) {//非空,建立實例
sWorld = new World();
}
}
}
return sWorld;
}
}
複製代碼
這樣不管多少個線程World都只會建立一次。雖然synchronized同步會影響一丟丟性能
不過進行了雙檢,只要有sWorld被建立了,是不會走同步的,測試了一下10000000
個線程經過第一檢的也就10幾個,因此這樣挺完美的。
指令重排序
一些時候
指令重排序
會將2和3步驟調換來提升性能。但並不是百分百都會重排序。
這在單線程中並無什麼威脅,但這裏多線程中sWorld == null
若是發生重排序,sWorld
指向內存空間,就會非空,若是實例化尚未來及。
下一個線程進入就會獲取到一個未初始化完成的對象,在使用它時會空指針異常。
解決方案很簡單在實例聲明時加上volatile關鍵字
便可。
原理:
Class對象的初始化鎖
。和上面的功能基本,因此我喜歡這個
public class World {
//[1]私有化構造
private World() {
initWorld();//初始化世界
System.out.println("世界已建立");
}
private void initWorld() {
}
//[3]返回內部靜態實例
public static World getInstance() {
return WorldHolder.sWorld;
}
//[2]建立內部類建立實例
private static class WorldHolder {
private static final World sWorld = new World();
}
}
複製代碼
枚舉默認私有化構造器,防反射,防反序列化。
public enum World {
INSTANCE;
World() {
initWorld();//初始化世界
System.out.println("世界已建立");
}
private void initWorld() {
}
}
複製代碼
關於枚舉:下面是經過jad反編譯獲得的枚舉源碼,可見枚舉在JVM的眼中也只是一個類而已,
而且私有化構造
+靜態代碼塊初始實例
,自然的單例材料。因爲靜態代碼塊初始實例,因此不是懶加載
命令:jad -s .java -8 World.class
package com.toly1994.dp.creational.singleton.world.enum_;
import java.io.PrintStream;
public final class World extends Enum{
public static World[] values(){
return (World[])$VALUES.clone();
}
public static World valueOf(String name){
return (World)Enum.valueOf(com/toly1994/dp/creational/singleton/world/enum_/World, name);
}
private World(String s, int i){
super(s, i);
initWorld();
System.out.println("\u4E16\u754C\u5DF2\u521B\u5EFA");
}
private void initWorld(){//私有化構造
}
public static final World INSTANCE;//靜態實例
private static final World $VALUES[];
static //靜態代碼塊初始實例
{
INSTANCE = new World("INSTANCE", 0);
$VALUES = (new World[] {
INSTANCE
});
}
}
複製代碼
單例的價值在於一個程序中只用一個該對象實例
若是有惡意份子經過反射建立了另外一個世界會怎麼樣?
經過debug看出兩次獲取的都是同一個世界,這就是單一實例
public class God {
public static void main(String[] args) {
World world1 = World.getInstance();
World world2 = World.getInstance();
}
}
複製代碼
反射
建立實例可見
world3
的內存地址已經不同了,說明出現了第二個世界,也就是單例的失效
public class God {
public static void main(String[] args) {
World world1 = World.getInstance();
World world2 = World.getInstance();
//經過反射建立
Class<World> worldClass = World.class;
try {
Constructor<World> constructor = worldClass.getDeclaredConstructor(null);
constructor.setAccessible(true);
World world3 = constructor.newInstance();
System.out.println(world3==world2);//false
System.out.println(world1==world2);//true
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製代碼
反序列化
建立對象若是你的單例類有序列化的需求(如,單例對象本地存儲,單例對象網絡傳輸) 反序列化造成的實例也並不是原來的實例
---->[World]-------------
public class World implements Serializable {
---->[God]-------------
public class God {
public static void main(String[] args) {
World world1 = World.getInstance();
World world2 = World.getInstance();
//經過反射建立
Class<World> worldClass = World.class;
try {
Constructor<World> constructor = worldClass.getDeclaredConstructor(null);
constructor.setAccessible(true);
World world3 = constructor.newInstance();
System.out.println(world3 == world2);//false
System.out.println(world1 == world2);//true
} catch (Exception e) {
e.printStackTrace();
}
//經過反序列化建立對象
try {
//序列化輸出
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("world.obj"));
oos.writeObject(world1);
//反序列化建立對象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("world.obj"));
World world4 = (World) ois.readObject();
ois.close();
System.out.println(world1 == world4);//false
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製代碼
經過反序列化時的鉤子函數:
readResolve
來控制序列化對象實例
---->[World]-------------
//解決反序列化建立實例的問題,readResolve建立的對象會直接替換io流讀取的對象
private Object readResolve() throws ObjectStreamException {
return getInstance();
}
複製代碼
[1] 肯定以及確定不會在單線程中用到的單例對象,能夠用單線程的懶漢
[2] 單例對象不大,並不介意在類加載時實例化對象,枚舉首選,其次是餓漢
[3] 若是要在多線程的時候徹底防反射,雙檢鎖模式不能夠。可以使用靜態初始化的幾種模式,在建立對象時進行非空校驗便可
複製代碼
java.util.Calendar 標準單例,經過Calendar.getInstance方法獲取對象
java.lang.System 徹底單例,不提供外部構造方法,所有以靜態方法提供服務
android.view.LayoutInflater 標準單例 ,經過LayoutInflater.from(Context)方法獲取對象
複製代碼
1----本文由張風捷特烈原創,轉載請註明
2----若是有什麼想要交流的,歡迎留言。也能夠加微信:zdl1994328
3----我的能力有限,若有不正之處歡迎你們批評指證,一定虛心改正
4----看到這裏,我在此感謝你的喜歡與支持,掃碼關注-編程之王