淺談單例模式和線程安全問題
單例模式、多實例模式、和線程安全
單例模式
單例模式是指確保一個類僅有一個唯一的實例,并且提供了一個全局的訪問點。
分類: 懶漢式、餓漢式
為什么需要單例模式?
再某些特殊的情況下,存在一個類僅能用來產(chǎn)生一個唯一對象的必要性。例如:打印機室有許多打印機,但是它的打印管理系統(tǒng)只有一個打印任務控制對象,該對象管理打印排隊并分配打印任務給各個打印機。單例模式正是為了解決這樣的需求而產(chǎn)生的。
實現(xiàn)思路:
為了防止客戶端利用構造器創(chuàng)建多個對象,將構造方法聲明為 private 類型。但這樣會使得這個類不可用,所以必須提供一個可以獲得實例的靜態(tài)方法,通常稱為 getInstance 方法, 該方法返回一個實例。這個方法必須是靜態(tài)的,因為靜態(tài)方法是根據(jù)類名調用的,否則也是無法使用的。
類圖:懶漢式

類圖:餓漢式

先來看一個簡單的例子:
測試單例類: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;
}
//靜態(tài)工廠方法
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;
}
//靜態(tài)工廠方法
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));
}
}
運行結果

懶漢式和餓漢式對比
創(chuàng)建區(qū)別
懶漢式是在第一次調用靜態(tài)方法 getInstance() 時創(chuàng)建單例對象。
餓漢式是在類加載時創(chuàng)建單例對象,即在聲明靜態(tài)單例對象時實例化單例類。
線程安全
懶漢式是線程不安全的,而餓漢式是線程安全的(下面會測試)。
資源占用
懶漢式是等到使用時才會創(chuàng)建,而餓漢式是在類加載時創(chuàng)建。所以懶漢式?jīng)]有餓漢式快,但是餓漢式比較占用資源,如果一直不使用,會很占據(jù)資源。
多線程模式下的安全性
多線程類
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 集合的特性,把每次產(chǎn)生的 dog 對象存入 Set集合中,最后只要調用集合的 size() 方法就行了??梢钥闯鰜懋a(chǎn)生了兩個 dog 對象,這就是產(chǎn)生了錯誤,這就是屬于編程錯誤了。還要明白多線程下不一定會出錯,所以產(chǎn)生的 dog 對象小于線程數(shù)。
由于 餓漢式單例 是線程安全的,這里就不測試了,有興趣的可以測試一下。
解決懶漢式單例線程安全的方法:同步
注意:同步有很多種方法,也可以使用 Lock 進行處理,同步是一種方法,不是特指 synchronzied 這個關鍵字,感興趣的人可以多探究一下。
并且同步的方法通常比較慢,性能方面也要權衡。
//靜態(tài)同步工廠方法
public synchronized static Dog getInstance() {
if (dog == null) {
dog = new Dog();
}
return dog;
}
多實例模式
這里補充一個多實例的模式,就是對象數(shù)量是固定數(shù)目的??梢钥闯鰡卫J降耐茝V。當然了實現(xiàn)方式也有很多,大家可以嘗試以下,這里是我的方式。
多實例模式類
//固定數(shù)目實例模式
public class MultiInstance {
//實例數(shù)量,這里為四個
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() {
//注意數(shù)組的下標只能為 COUNT - 1
if (MultiInstance.COUNT <= MultiInstance.INSTANCE_COUNT - 1) {
instance[MultiInstance.COUNT] = new MultiInstance();
MultiInstance.COUNT++;
}
//返回實例前,執(zhí)行了 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();
}
//測試多實例模式(單例的擴展,固定數(shù)目實例)
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());
}
}
運行結果
注意:如果在多線程環(huán)境下使用,也是要考慮線程安全的。感興趣的可以自己實現(xiàn)一下。

單例模式一定是安全的嗎?
不一定,有很多方法可以破壞單例模式!
這里舉例看一看(我只能舉我知道的哈!其他的感興趣,可以去探究一下?。?br />使用反射:這種辦法是非常有用的,通過反射即使是私有的屬性和方法也可以訪問了,因此反射破壞了類的封裝性,所以使用反射還是要多多小心。但是反射也有許多其他的用途,這是一項非常有趣的技術(我也只是會一點點)。
使用反射破壞單例模式測試類
這里使用的還是前面的 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(增加一個對象序列化接口實現(xiàn))
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;
}
//靜態(tài)工廠方法
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));
}
}
運行結果

說明
這里可以看出來通過對象序列化(這里也可以說是對象的深拷貝或深克隆),
同樣也可以實現(xiàn)類的實例的不唯一性。這同樣也算是破壞了類的封裝性。對象序列化和反序列化的過程中,對象的唯一性變了。
這里具體的原因很復雜,我最近看了點深拷貝的知識,所以只是知其然不知其之所以然。(所以學習是需要不斷進行的!加油諸位。)
這里我貼一下別的經(jīng)驗吧:(感興趣的可以實現(xiàn)一下?。?/p>
為什么序列化可以破壞單例了?
答:序列化會通過反射調用無參數(shù)的構造方法創(chuàng)建一個新的對象。
這個東西目前超出了我的能力范圍了,但也是去查看源碼得出來的,就是序列化(serializable)和反序列化(externalizable)接口的詳細情況了。但是有一點,它也是通過反射來做的的,所以可以看出**反射(reflect)**是一種非常強大和危險的技術了。
總結
單例模式 是很有趣的,它涉及了很多知識,所以大家學習的時候,不要只滿足與課本的知識,如果只是會使用簡單的 單例模式,那是沒有什么核心競爭力的,任何一個知識,只要往下深究都是不容易的,我也只是一個初學者,希望和大家一起努力進步。
到此這篇關于淺談單例模式和線程安全問題的文章就介紹到這了,更多相關單例模式和線程安全內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java數(shù)據(jù)結構與算法之棧(Stack)實現(xiàn)詳解
這篇文章主要為大家詳細介紹了Java數(shù)據(jù)結構學習筆記第二篇,Java數(shù)據(jù)結構與算法之棧Stack實現(xiàn),具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-09-09
一步步教你如何使用Java實現(xiàn)WebSocket
websocket協(xié)議是基于TCP的一種新的網(wǎng)絡協(xié)議,它實現(xiàn)了瀏覽器與服務器的全雙工通訊-允許服務器主動發(fā)起信息個客戶端,websocket是一種持久協(xié)議,http是非持久協(xié)議,下面這篇文章主要給大家介紹了關于如何使用Java實現(xiàn)WebSocket的相關資料,需要的朋友可以參考下2023-05-05
Maven中怎么手動添加jar包到本地倉庫詳解(repository)
這篇文章主要給大家介紹了關于Maven中怎么手動添加jar包到本地倉庫的相關資料,文中通過圖文以及實例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2023-04-04

