首頁  >  文章  >  Java  >  java單例模式和線程安全問題怎麼解決

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

王林
王林轉載
2023-05-12 23:07:041444瀏覽

    單例模式、多實例模式、和執行緒安全性

    #單例模式

    單例模式是指確保一個類別僅有一個唯一的實例,並且提供了一個全域的存取點。

    分類: 懶漢式、餓漢式

    為什麼需要單例模式?

    再某些特殊的情況下,存在一個類別僅能用來產生一個唯一物件的必要性。例如:印表機室有許多印表機,但是它的列印管理系統只有一個列印任務控制對象,該對像管理列印排隊並分配列印任務給各個印表機。單例模式正是為了解決這樣的需求而產生的。

    實作想法:

    為了防止客戶端利用建構器建立多個對象,將建構方法宣告為 private 類型。但這樣會使得這個類別不可用,所以必須提供一個可以獲得實例的靜態方法,通常稱為 getInstance 方法, 此方法傳回一個實例。這個方法必須是靜態的,因為靜態方法是根據類別名稱呼叫的,否則也是無法使用的。

    類別圖:懶漢式

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

    #類別圖:餓漢式

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

    #先來看一個簡單的範例:

    測試單例類別: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));
    	}
    }

    運行結果

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

    ##懶漢式和餓漢式對比

    創建區別

    懶漢式是在第一次呼叫

    靜態方法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&#39;s number: "+dogThread.getCount());
    	}
    }

    運行結果
    注意:多執行緒的結果是很難預測的,這裡涉及執行緒的競爭,可能多次運行結果是一樣的(多次一樣不代表是絕對正確),但是只要多次測試,就能看到不一樣的結果。

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

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

    說明

    這裡我使用一點集合的技巧,利用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());   
    	}
    }

    執行結果注意:如果在多執行緒環境下使用,也是要考慮執行緒安全的。有興趣的可以自己實現一下。

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

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

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

    这里举例看一看(我只能举我知道的哈!其他的感兴趣,可以去探究一下!)
    使用反射:这种办法是非常有用的,通过反射即使是私有的属性和方法也可以访问了,因此反射破坏了类的封装性,所以使用反射还是要多多小心。但是反射也有许多其他的用途,这是一项非常有趣的技术(我也只是会一点点)。

    使用反射破坏单例模式测试类

    这里使用的还是前面的 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));
    		
    	}
    }

    运行结果

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

    说明
    这里可以看出来通过对象序列化(这里也可以说是对象的深拷贝或深克隆),
    同样也可以实现类的实例的不唯一性。这同样也算是破坏了类的封装性。对象序列化和反序列化的过程中,对象的唯一性变了。

    这里具体的原因很复杂,我最近看了点深拷贝的知识,所以只是知其然不知其之所以然。(所以学习是需要不断进行的!加油诸位。)
    这里我贴一下别的经验吧:(感兴趣的可以实现一下!)

    为什么序列化可以破坏单例了?
    答:序列化会通过反射调用无参数的构造方法创建一个新的对象。

    这个东西目前超出了我的能力范围了,但也是去查看源码得出来的,就是序列化(serializable)和反序列化(externalizable)接口的详细情况了。但是有一点,它也是通过反射来做的的,所以可以看出**反射(reflect)**是一种非常强大和危险的技术了。

    以上是java單例模式和線程安全問題怎麼解決的詳細內容。更多資訊請關注PHP中文網其他相關文章!

    陳述:
    本文轉載於:yisu.com。如有侵權,請聯絡admin@php.cn刪除