您好,登錄后才能下訂單哦!
這篇文章主要介紹“java單例模式和線程安全問題怎么解決”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“java單例模式和線程安全問題怎么解決”文章能幫助大家解決問題。
單例模式是指確保一個類僅有一個唯一的實例,并且提供了一個全局的訪問點。
分類: 懶漢式、餓漢式
為什么需要單例模式?
再某些特殊的情況下,存在一個類僅能用來產生一個唯一對象的必要性。例如:打印機室有許多打印機,但是它的打印管理系統只有一個打印任務控制對象,該對象管理打印排隊并分配打印任務給各個打印機。單例模式正是為了解決這樣的需求而產生的。
實現思路:
為了防止客戶端利用構造器創建多個對象,將構造方法聲明為 private 類型。但這樣會使得這個類不可用,所以必須提供一個可以獲得實例的靜態方法,通常稱為 getInstance 方法, 該方法返回一個實例。這個方法必須是靜態的,因為靜態方法是根據類名調用的,否則也是無法使用的。
類圖:懶漢式
類圖:餓漢式
先來看一個簡單的例子:
測試單例類:Dog’
//懶漢式 public class Dog { private static Dog dog; private String name; private int age; //私有的構造器 private Dog() {} public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } //靜態工廠方法 public static Dog getInstance() { if (dog == null) { dog = new Dog(); } return dog; } @Override public String toString() { return "Dog [name=" + name + ", age=" + age + "]"; } }
測試單例類:Cat
//餓漢式 public class Cat { private static Cat cat = new Cat(); private String name; private int age; //私有構造器 private Cat() {} public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } //靜態工廠方法 public static Cat getInstance() { return cat; } @Override public String toString() { return "Cat [name=" + name + ", age=" + age + "]"; } }
測試類
import java.util.HashSet; import java.util.Set; public class Client { public static void main(String[] args) { //單線程模式測試 Dog dog1 = Dog.getInstance(); Dog dog2 = Dog.getInstance(); System.out.println("dog1 == dog2: "+(dog1 == dog2)); Cat cat1 = Cat.getInstance(); Cat cat2 = Cat.getInstance(); System.out.println("cat1 == cat2: "+(cat1 == cat2)); } }
運行結果
創建區別
懶漢式是在第一次調用靜態方法 getInstance() 時創建單例對象。
餓漢式是在類加載時創建單例對象,即在聲明靜態單例對象時實例化單例類。
線程安全
懶漢式是線程不安全的,而餓漢式是線程安全的(下面會測試)。
資源占用
懶漢式是等到使用時才會創建,而餓漢式是在類加載時創建。所以懶漢式沒有餓漢式快,但是餓漢式比較占用資源,如果一直不使用,會很占據資源。
多線程類
import java.util.HashSet; import java.util.Set; public class DogThread extends Thread{ private Dog dog; private Set<Dog> set; public DogThread() { set = new HashSet<>(); } //這個方法是為了測試添加的。 public int getCount() { return set.size(); } @Override public void run() { dog = Dog.getInstance(); set.add(dog); } }
多線程測試類
import java.util.HashSet; import java.util.Set; public class Client { public static void main(String[] args) { //單線程模式測試 Dog dog1 = Dog.getInstance(); Dog dog2 = Dog.getInstance(); System.out.println("dog1 == dog2: "+(dog1 == dog2)); Cat cat1 = Cat.getInstance(); Cat cat2 = Cat.getInstance(); System.out.println("cat1 == cat2: "+(cat1 == cat2)); //多線程模式測試 DogThread dogThread = new DogThread(); Thread thread = null; for (int i = 0; i < 10; i++) { thread = new Thread(dogThread); thread.start(); } try { Thread.sleep(2000); //主線程等待子線程完成! } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("dog's number: "+dogThread.getCount()); } }
運行結果
注意:多線程的結果是很難預測的,這里涉及線程的競爭,可能多次運行結果是一樣的(多次一樣并不代表是絕對正確),但是只要多次測試,就能看到不一樣的結果。
說明
這里我使用一點集合的技巧,利用 Set 集合的特性,把每次產生的 dog 對象存入 Set集合中,最后只要調用集合的 size() 方法就行了。可以看出來產生了兩個 dog 對象,這就是產生了錯誤,這就是屬于編程錯誤了。還要明白多線程下不一定會出錯,所以產生的 dog 對象小于線程數。
由于 餓漢式單例 是線程安全的,這里就不測試了,有興趣的可以測試一下。
解決懶漢式單例線程安全的方法:同步
注意:同步有很多種方法,也可以使用 Lock 進行處理,同步是一種方法,不是特指 synchronzied 這個關鍵字,感興趣的人可以多探究一下。
并且同步的方法通常比較慢,性能方面也要權衡。
//靜態同步工廠方法 public synchronized static Dog getInstance() { if (dog == null) { dog = new Dog(); } return dog; }
這里補充一個多實例的模式,就是對象數量是固定數目的。可以看出單例模式的推廣。當然了實現方式也有很多,大家可以嘗試以下,這里是我的方式。
多實例模式類
//固定數目實例模式 public class MultiInstance { //實例數量,這里為四個 private final static int INSTANCE_COUNT = 4; private static int COUNT = 0; private static MultiInstance[] instance = new MultiInstance[4]; private MultiInstance() {}; public static MultiInstance getInstance() { //注意數組的下標只能為 COUNT - 1 if (MultiInstance.COUNT <= MultiInstance.INSTANCE_COUNT - 1) { instance[MultiInstance.COUNT] = new MultiInstance(); MultiInstance.COUNT++; } //返回實例前,執行了 COUNT++ 操作,所以 應該返回上一個實例 return MultiInstance.instance[MultiInstance.COUNT-1]; } }
測試類
import java.util.HashSet; import java.util.Set; public class Test { public static void main(String[] args) { System.out.println("------------------------"); testMultiInstance(); } //測試多實例模式(單例的擴展,固定數目實例) public static void testMultiInstance() { Set<MultiInstance> instanceSet = new HashSet<>(); MultiInstance instance = null; for (int i = 0; i < 10; i++) { instance = MultiInstance.getInstance(); instanceSet.add(instance); } System.out.println("8個實例中,不同的實例有:"+instanceSet.size()); } }
運行結果
注意:如果在多線程環境下使用,也是要考慮線程安全的。感興趣的可以自己實現一下。
單例模式一定是安全的嗎?
不一定,有很多方法可以破壞單例模式!
這里舉例看一看(我只能舉我知道的哈!其他的感興趣,可以去探究一下!)
使用反射:這種辦法是非常有用的,通過反射即使是私有的屬性和方法也可以訪問了,因此反射破壞了類的封裝性,所以使用反射還是要多多小心。但是反射也有許多其他的用途,這是一項非常有趣的技術(我也只是會一點點)。
使用反射破壞單例模式測試類
這里使用的還是前面的 Dog 實體類。注意我這里的**包名:**com。
所有的類都是在 com包 下面的。
import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; public class Client { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { Class<?> clazz = Class.forName("com.Dog"); Constructor<?> con = clazz.getDeclaredConstructor(); //設置可訪問權限 con.setAccessible(true); Dog dog1 = (Dog) con.newInstance(); Dog dog2 = (Dog) con.newInstance(); System.out.println(dog1 == dog2); } }
說明:反射的功能是很強大的,從這里既可以看出來,正是有了反射,才使得Java 語言具有了更多的特色,這也是Java的強大之處。
使用對象序列化破壞單例模式
測試實體類:Dog(增加一個對象序列化接口實現)
import java.io.Serializable; //懶漢式 public class Dog implements Serializable{ private static final long serialVersionUID = 1L; private static Dog dog; private String name; private int age; //私有的構造器 private Dog() {} public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } //靜態工廠方法 public synchronized static Dog getInstance() { if (dog == null) { dog = new Dog(); } return dog; } @Override public String toString() { return "Dog [name=" + name + ", age=" + age + "]"; } }
對象序列化測試類
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class Client { public static void main(String[] args) throws IOException, ClassNotFoundException { Dog dog1 = Dog.getInstance(); dog1.setName("小黑"); dog1.setAge(2); System.out.println(dog1.toString()); ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(dog1); ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); Dog dog2 = (Dog) ois.readObject(); System.out.println(dog2.toString()); System.out.println("dog1 == dog2: "+(dog1 == dog2)); } }
運行結果
說明
這里可以看出來通過對象序列化(這里也可以說是對象的深拷貝或深克隆),
同樣也可以實現類的實例的不唯一性。這同樣也算是破壞了類的封裝性。對象序列化和反序列化的過程中,對象的唯一性變了。
這里具體的原因很復雜,我最近看了點深拷貝的知識,所以只是知其然不知其之所以然。(所以學習是需要不斷進行的!加油諸位。)
這里我貼一下別的經驗吧:(感興趣的可以實現一下!)
為什么序列化可以破壞單例了?
答:序列化會通過反射調用無參數的構造方法創建一個新的對象。
這個東西目前超出了我的能力范圍了,但也是去查看源碼得出來的,就是序列化(serializable)和反序列化(externalizable)接口的詳細情況了。但是有一點,它也是通過反射來做的的,所以可以看出**反射(reflect)**是一種非常強大和危險的技術了。
關于“java單例模式和線程安全問題怎么解決”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。