浅谈单例模式和线程安全问题

Java
244
0
0
2023-09-16
目录
  • 单例模式、多实例模式、和线程安全
  • 单例模式
  • 懒汉式和饿汉式对比
  • 多线程模式下的安全性
  • 多实例模式
  • 总结

单例模式、多实例模式、和线程安全

单例模式

单例模式是指确保一个类仅有一个唯一的实例,并且提供了一个全局的访问点。

分类: 懒汉式、饿汉式

为什么需要单例模式?

再某些特殊的情况下,存在一个类仅能用来产生一个唯一对象的必要性。例如:打印机室有许多打印机,但是它的打印管理系统只有一个打印任务控制对象,该对象管理打印排队并分配打印任务给各个打印机。单例模式正是为了解决这样的需求而产生的。

实现思路:

为了防止客户端利用构造器创建多个对象,将构造方法声明为 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 dog = Dog.getInstance();
		Dog dog = Dog.getInstance();
		System.out.println("dog == dog2: "+(dog1 == dog2));
		
		Cat cat = Cat.getInstance();
		Cat cat = Cat.getInstance();
		System.out.println("cat == 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 dog = Dog.getInstance();
		Dog dog = Dog.getInstance();
		System.out.println("dog == dog2: "+(dog1 == dog2));
		
		Cat cat = Cat.getInstance();
		Cat cat = Cat.getInstance();
		System.out.println("cat == cat2: "+(cat1 == cat2));
		
		//多线程模式测试
		DogThread dogThread = new DogThread();
		Thread thread = null;
		for (int i =; i < 10; i++) {
			thread = new Thread(dogThread);
			thread.start();	
		}
		
		try {
			Thread.sleep(); //主线程等待子线程完成!
		} 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 =;
	private static int COUNT =;
	private static MultiInstance[] instance = new MultiInstance[];
	
	private MultiInstance() {};
	
	public static MultiInstance getInstance() {
		//注意数组的下标只能为 COUNT -
		if (MultiInstance.COUNT <= MultiInstance.INSTANCE_COUNT -) {
			instance[MultiInstance.COUNT] = new MultiInstance();
			MultiInstance.COUNT++;
		}
		//返回实例前,执行了 COUNT++ 操作,所以 应该返回上一个实例
		return MultiInstance.instance[MultiInstance.COUNT-];  
	}
}

测试类

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 =; i < 10; i++) {
			instance = MultiInstance.getInstance();
			instanceSet.add(instance);
		}
		System.out.println("个实例中,不同的实例有:"+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 dog = (Dog) con.newInstance();
		Dog dog = (Dog) con.newInstance();
		System.out.println(dog == dog2);
	}
}

说明:反射的功能是很强大的,从这里既可以看出来,正是有了反射,才使得Java 语言具有了更多的特色,这也是Java的强大之处。

使用对象序列化破坏单例模式

测试实体类:Dog(增加一个对象序列化接口实现)

import java.io.Serializable;
//懒汉式
public class Dog implements Serializable{
	private static final long serialVersionUID =L;
	
	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 dog = Dog.getInstance();
		dog.setName("小黑");
		dog.setAge(2);
		System.out.println(dog.toString());
		
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		ObjectOutputStream oos = new ObjectOutputStream(bos);
		oos.writeObject(dog);
		
		
		ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
		ObjectInputStream ois = new ObjectInputStream(bis);
		Dog dog = (Dog) ois.readObject();
		System.out.println(dog.toString());
		System.out.println("dog == dog2: "+(dog1 == dog2));
		
	}
}

运行结果

在这里插入图片描述

说明

这里可以看出来通过对象序列化(这里也可以说是对象的深拷贝或深克隆),

同样也可以实现类的实例的不唯一性。这同样也算是破坏了类的封装性。对象序列化和反序列化的过程中,对象的唯一性变了。

这里具体的原因很复杂,我最近看了点深拷贝的知识,所以只是知其然不知其之所以然。(所以学习是需要不断进行的!加油诸位。)

这里我贴一下别的经验吧:(感兴趣的可以实现一下!)

为什么序列化可以破坏单例了?

答:序列化会通过反射调用无参数的构造方法创建一个新的对象。

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

总结

单例模式 是很有趣的,它涉及了很多知识,所以大家学习的时候,不要只满足与课本的知识,如果只是会使用简单的 单例模式,那是没有什么核心竞争力的,任何一个知识,只要往下深究都是不容易的,我也只是一个初学者,希望和大家一起努力进步。