溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

java單例模式和線程安全問題怎么解決

發(fā)布時(shí)間:2023-04-07 10:59:24 來源:億速云 閱讀:140 作者:iii 欄目:開發(fā)技術(shù)

這篇文章主要介紹“java單例模式和線程安全問題怎么解決”的相關(guān)知識(shí),小編通過實(shí)際案例向大家展示操作過程,操作方法簡(jiǎn)單快捷,實(shí)用性強(qiáng),希望這篇“java單例模式和線程安全問題怎么解決”文章能幫助大家解決問題。

    單例模式、多實(shí)例模式、和線程安全

    單例模式

    單例模式是指確保一個(gè)類僅有一個(gè)唯一的實(shí)例,并且提供了一個(gè)全局的訪問點(diǎn)。

    分類: 懶漢式、餓漢式

    為什么需要單例模式?

    再某些特殊的情況下,存在一個(gè)類僅能用來產(chǎn)生一個(gè)唯一對(duì)象的必要性。例如:打印機(jī)室有許多打印機(jī),但是它的打印管理系統(tǒng)只有一個(gè)打印任務(wù)控制對(duì)象,該對(duì)象管理打印排隊(duì)并分配打印任務(wù)給各個(gè)打印機(jī)。單例模式正是為了解決這樣的需求而產(chǎn)生的。

    實(shí)現(xiàn)思路:

    為了防止客戶端利用構(gòu)造器創(chuàng)建多個(gè)對(duì)象,將構(gòu)造方法聲明為 private 類型。但這樣會(huì)使得這個(gè)類不可用,所以必須提供一個(gè)可以獲得實(shí)例的靜態(tài)方法,通常稱為 getInstance 方法, 該方法返回一個(gè)實(shí)例。這個(gè)方法必須是靜態(tài)的,因?yàn)殪o態(tài)方法是根據(jù)類名調(diào)用的,否則也是無法使用的。

    類圖:懶漢式

    java單例模式和線程安全問題怎么解決

    類圖:餓漢式

    java單例模式和線程安全問題怎么解決

    先來看一個(gè)簡(jiǎn)單的例子:

    測(cè)試單例類:Dog’

    //懶漢式
    public class Dog {
    	private static Dog dog;
    	private String name;
    	private int age;
    	
    	//私有的構(gòu)造器
    	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 + "]";
    	}
    }

    測(cè)試單例類:Cat

    //餓漢式
    public class Cat {
    	private static Cat cat = new Cat();
    	private String name;
    	private int age;
    	
    	//私有構(gòu)造器
    	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 + "]";
    	}
    }

    測(cè)試類

    import java.util.HashSet;
    import java.util.Set;
    
    public class Client {
    
    	public static void main(String[] args) {
    		//單線程模式測(cè)試
    		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));
    	}
    }

    運(yùn)行結(jié)果

    java單例模式和線程安全問題怎么解決

    懶漢式和餓漢式對(duì)比

    創(chuàng)建區(qū)別

    懶漢式是在第一次調(diào)用靜態(tài)方法 getInstance() 時(shí)創(chuàng)建單例對(duì)象。
    餓漢式是在類加載時(shí)創(chuàng)建單例對(duì)象,即在聲明靜態(tài)單例對(duì)象時(shí)實(shí)例化單例類。

    線程安全

    懶漢式是線程不安全的,而餓漢式是線程安全的(下面會(huì)測(cè)試)。

    資源占用

    懶漢式是等到使用時(shí)才會(huì)創(chuàng)建,而餓漢式是在類加載時(shí)創(chuàng)建。所以懶漢式?jīng)]有餓漢式快,但是餓漢式比較占用資源,如果一直不使用,會(huì)很占據(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<>();
    	}
    	
    	//這個(gè)方法是為了測(cè)試添加的。
    	public int getCount() {
    		return set.size();
    	}
    	
    	@Override
    	public void run() {
    		dog = Dog.getInstance();
    		set.add(dog);
    	}
    }

    多線程測(cè)試類

    import java.util.HashSet;
    import java.util.Set;
    
    public class Client {
    
    	public static void main(String[] args) {
    		//單線程模式測(cè)試
    		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));
    		
    		//多線程模式測(cè)試
    		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());
    	}
    }

    運(yùn)行結(jié)果
    注意:多線程的結(jié)果是很難預(yù)測(cè)的,這里涉及線程的競(jìng)爭(zhēng),可能多次運(yùn)行結(jié)果是一樣的(多次一樣并不代表是絕對(duì)正確),但是只要多次測(cè)試,就能看到不一樣的結(jié)果。

    java單例模式和線程安全問題怎么解決

    java單例模式和線程安全問題怎么解決

    說明

    這里我使用一點(diǎn)集合的技巧,利用 Set 集合的特性,把每次產(chǎn)生的 dog 對(duì)象存入 Set集合中,最后只要調(diào)用集合的 size() 方法就行了??梢钥闯鰜懋a(chǎn)生了兩個(gè) dog 對(duì)象,這就是產(chǎn)生了錯(cuò)誤,這就是屬于編程錯(cuò)誤了。還要明白多線程下不一定會(huì)出錯(cuò),所以產(chǎn)生的 dog 對(duì)象小于線程數(shù)。
    由于 餓漢式單例 是線程安全的,這里就不測(cè)試了,有興趣的可以測(cè)試一下。

    解決懶漢式單例線程安全的方法:同步
    注意:同步有很多種方法,也可以使用 Lock 進(jìn)行處理,同步是一種方法,不是特指 synchronzied 這個(gè)關(guān)鍵字,感興趣的人可以多探究一下。
    并且同步的方法通常比較慢,性能方面也要權(quán)衡。

    	//靜態(tài)同步工廠方法
    	public synchronized static Dog getInstance() {
    		if (dog == null) {
    			dog = new Dog();
    		}
    		return dog;
    	}

    多實(shí)例模式

    這里補(bǔ)充一個(gè)多實(shí)例的模式,就是對(duì)象數(shù)量是固定數(shù)目的??梢钥闯鰡卫J降耐茝V。當(dāng)然了實(shí)現(xiàn)方式也有很多,大家可以嘗試以下,這里是我的方式。

    多實(shí)例模式類

    //固定數(shù)目實(shí)例模式
    public class MultiInstance {
    	//實(shí)例數(shù)量,這里為四個(gè)
    	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ù)組的下標(biāo)只能為 COUNT - 1
    		if (MultiInstance.COUNT <= MultiInstance.INSTANCE_COUNT - 1) {
    			instance[MultiInstance.COUNT] = new MultiInstance();
    			MultiInstance.COUNT++;
    		}
    		//返回實(shí)例前,執(zhí)行了 COUNT++ 操作,所以 應(yīng)該返回上一個(gè)實(shí)例
    		return MultiInstance.instance[MultiInstance.COUNT-1];  
    	}
    }

    測(cè)試類

    import java.util.HashSet;
    import java.util.Set;
    
    public class Test {
    	public static void main(String[] args) {
    		
    		System.out.println("------------------------");
    		testMultiInstance();
    	}
    
    	//測(cè)試多實(shí)例模式(單例的擴(kuò)展,固定數(shù)目實(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個(gè)實(shí)例中,不同的實(shí)例有:"+instanceSet.size());   
    	}
    }

    運(yùn)行結(jié)果
    注意:如果在多線程環(huán)境下使用,也是要考慮線程安全的。感興趣的可以自己實(shí)現(xiàn)一下。

    java單例模式和線程安全問題怎么解決

    單例模式一定是安全的嗎?

    不一定,有很多方法可以破壞單例模式!

    這里舉例看一看(我只能舉我知道的哈!其他的感興趣,可以去探究一下?。?br/>使用反射:這種辦法是非常有用的,通過反射即使是私有的屬性和方法也可以訪問了,因此反射破壞了類的封裝性,所以使用反射還是要多多小心。但是反射也有許多其他的用途,這是一項(xiàng)非常有趣的技術(shù)(我也只是會(huì)一點(diǎn)點(diǎn))。

    使用反射破壞單例模式測(cè)試類

    這里使用的還是前面的 Dog 實(shí)體類。注意我這里的**包名:**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();
    		//設(shè)置可訪問權(quán)限
    		con.setAccessible(true);
    		Dog dog1 = (Dog) con.newInstance();
    		Dog dog2 = (Dog) con.newInstance();
    		System.out.println(dog1 == dog2);
    	}
    }

    說明:反射的功能是很強(qiáng)大的,從這里既可以看出來,正是有了反射,才使得Java 語言具有了更多的特色,這也是Java的強(qiáng)大之處。

    使用對(duì)象序列化破壞單例模式

    測(cè)試實(shí)體類:Dog(增加一個(gè)對(duì)象序列化接口實(shí)現(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;
    	
    	//私有的構(gòu)造器
    	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 + "]";
    	}
    }

    對(duì)象序列化測(cè)試類

    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));
    		
    	}
    }

    運(yùn)行結(jié)果

    java單例模式和線程安全問題怎么解決

    說明
    這里可以看出來通過對(duì)象序列化(這里也可以說是對(duì)象的深拷貝或深克隆),
    同樣也可以實(shí)現(xiàn)類的實(shí)例的不唯一性。這同樣也算是破壞了類的封裝性。對(duì)象序列化和反序列化的過程中,對(duì)象的唯一性變了。

    這里具體的原因很復(fù)雜,我最近看了點(diǎn)深拷貝的知識(shí),所以只是知其然不知其之所以然。(所以學(xué)習(xí)是需要不斷進(jìn)行的!加油諸位。)
    這里我貼一下別的經(jīng)驗(yàn)吧:(感興趣的可以實(shí)現(xiàn)一下?。?/p>

    為什么序列化可以破壞單例了?
    答:序列化會(huì)通過反射調(diào)用無參數(shù)的構(gòu)造方法創(chuàng)建一個(gè)新的對(duì)象。

    這個(gè)東西目前超出了我的能力范圍了,但也是去查看源碼得出來的,就是序列化(serializable)和反序列化(externalizable)接口的詳細(xì)情況了。但是有一點(diǎn),它也是通過反射來做的的,所以可以看出**反射(reflect)**是一種非常強(qiáng)大和危險(xiǎn)的技術(shù)了。

    關(guān)于“java單例模式和線程安全問題怎么解決”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí),可以關(guān)注億速云行業(yè)資訊頻道,小編每天都會(huì)為大家更新不同的知識(shí)點(diǎn)。

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI