我的个人博客Alexios,欢迎大家来吐槽交流。
1、单例模式介绍
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
下面介绍几种单例模式的实现方法
2、饿汉式
这种单例模式实现简单,且在多线程环境下保证了并发全,但缺点是无法实现懒加载,方法单例在类加载的时候就会被创建,可能会造成空间浪费,这种单例模式会被反射和序列化工具破坏。
实现思路
- 在类中new 一个静态类对象instance
- 私有化构造方法
- 编写一个静态方法用于返回第一步new的instance对象
public class Singleton01 implements Serializable {
private static Singleton01 instance = new Singleton01();
private Singleton01() {}
public static Singleton01 getInstance() {
return instance;
}
}
这个实现方法在高并发下线程安全,但可以被反射和序列化工具破坏,下面一一进行测试
2.1、使用反射破坏
- 测试代码
Class clazz = Singleton01.class;
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
//使用反射获取的构造器新建的对象
Singleton01 singleton01 = (Singleton01) constructor.newInstance();
//使用单例类方法获取的对象
Singleton01 instance = getInstance();
//比较地址
System.out.println(singleton01 == instance);
- 结果
2.2、使用序列化工具进行破坏,下面给出序列化工具及演示代码
- 序列化工具类
public class SerializationUtils {
/**
* 序列化对象
*
* @param obj 对象
* @return 序列化后的字节数组
* @throws IOException
*/
public static byte[] serialize(Object obj) throws IOException {
if (null == obj) {
return null;
}
try (
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteArrayOutputStream);
) {
out.writeObject(obj);
return byteArrayOutputStream.toByteArray();
}
}
/**
* 反序列化
*
* @param bytes 对象序列化后的字节数组
* @return 反序列化后的对象
* @throws IOException
* @throws ClassNotFoundException
*/
public static Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
if (null == bytes) {
return null;
}
try (
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream in = new ObjectInputStream(byteArrayInputStream);
) {
return in.readObject();
}
}
}
- 测试代码
//获取单例对象并进行序列化
Singleton01 instance = getInstance();
byte[] bytes = SerializationUtils.serialize(instance);
//进行反序列化,获取instance对象
Singleton01 deserializeInstance = (Singleton01)SerializationUtils.deserialize(bytes);
//比较两个对象的地址
System.out.println(instance);
System.out.println(deserializeInstance);
System.out.println(instance == deserializeInstance);
- 结果
2.3、问题解决方案
对于反射,我们可以在私有的构造函数中做一些处理,当用户进入饿汉式单例模式的构造方法时,我们可以判断当前静态对象instance是否为空,如果不为空,直接抛出一个异常,让其他人不能用反射来破坏单例模式。
- 代码实现
private Singleton01() {
if(instance != null) {
throw new IllegalStateException();
}
}
- 再次执行上面的测试代码,结果为
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vMVJkqTa-1612266740278)(https://raw.githubusercontent.com/tianxin763882220/images/master/blog/20201208151900.png)]
由于类的静态成员的序列化问题,所以每次反序列化都会new一个类对象,我们可以写一个readReslove方法来时其序列化和反序列化使用的都是同一个instance对象。
- 代码实现
public Object readResolve() {
return instance;
}
- 结果
3、饿汉式单例模式的改进–登记式
登记式单例模式采用了一个静态内部类来持有该类的单实例对象,在外部类被加载时,静态内部类不会被加载,即类的单例对象不会被创建,当用户第一次使用类的getInstance方法时,类的单实例才会被加载,这样既保证了线程安全,又起到了懒加载的效果。
3.1、登记式实现思路
- 在类中添加一个静态内部类SingtonHolder,类中存放外部类的唯一实例instance
- 私有化构造器,在构造器中通过判断静态内部类中instance实例是否为空来决定要不要抛出异常。
- 在外部类中构建一个静态方法getInstance来返回类的单实例。
- 写一个readReslove方法来防止序列化破坏单例。
3.2、代码实现
public class Singleton02 implements Serializable {
private static class SingletonHolder {
public static Singleton02 instance = new Singleton02();
}
private Singleton02() {
if(SingletonHolder.instance != null) {
throw new IllegalStateException();
}
}
public static Singleton02 getInstance() {
return SingletonHolder.instance;
}
public Object readResolve() {
return SingletonHolder.instance;
}
}
静态内部类的加载时机,我们可以在Sington2的构造函数中输出一段语句来判断静态内部类的加载时间。
Class clazz = Class.forName("singleton.Singleton02");
System.out.println("-----------------------------------");
Singleton02 instance = getInstance();
- 结果
- 结论:静态内部类在外部类加载时不会被加载,而是在用户第一次调用getInstance方法时才加载。
小结:对于饿汉式,推荐使用登记式。
4、懒汉式和双检锁
懒汉式单例模式意味着在类加载时不创建类的单实例对象,而是当用户需要实例时才去创建对象,这种方法实现了懒加载,且实现简单,但不能保证多线程环境下的单例唯一性。
4.1、代码实现
public class Singleton03 {
private static Singleton03 instance;
private Singleton03() {}
public Singleton03 getInstance() {
if(instance == null) {
instance = new Singleton03();
}
return instance;
}
}
这种单例模式存在许多问题,除了前面提到的可以用反射和序列化工具破坏外,这种单例模式在多线程并发环境下不能保证单例的唯一性,给出测试代码
for (int i = 0; i < 200; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(getInstance());
}
}).start();
}
- 结果:可以看到出现了两个对象,所以需要对这种模式进行改进
4.2、解决懒汉式在多线程并发下的线程安全问题
在getInstance方法中添加一个synchronizd块锁定代码,就可以解决线程安全问题,下面给出代码实现
public static Singleton04 getInstance() {
synchronized (Singleton04.class) {
if(instance == null) {
instance = new Singleton04();
}
}
return instance;
}
这样就可以保证多线程下的线程安全问题,但这种直接加同步块的代码会影响程序的性能,所以我们可以通过引进双检锁模式的方法在保证线程安全的前提下尽可能地保证性能。
4.3、双检锁
同样对getInstance方法进行改造,在同步块外再次添加一个判断,即单例对象为空时,才进入同步块及以后代码,代码实现如下
public static DoubleCheckLock getInstance() {
if(instance == null) {
synchronized (DoubleCheckLock.class) {
if(instance == null) {
instance = new DoubleCheckLock();
}
}
}
return instance;
}
假设AB同时进入第一个if判断,由于synchronized块的存在,AB有一个线程会进入同步块中,其他线程等待,进入同步块的线程判断当前单实例对象为空,于是会初始化对象,然后退出synchronized块,此时另外的线程进入代码块时,由于instance已经被初始化,所以会直接返回已经初始化的instance。之后的线程在进入synchronized块时会先进行一次判断,此时的instance已经不为空,所以直接返回instance。
- 可能遇到的问题
由于JVM机存在指令重排,同时初始化instance对象(instance = new DoubleCheckLock())执行的操作如下
1 分配对象内存空间
2 初始化对象
3 instance指向1分配的空间
出现指令重排后,执行的操作如下
1 分配对象内存空间
2 instance指向1分配的空间
3 初始化对象
如果此时其他线程在指令重排的第2步就对instance进行判断,那么可能拿到一个空的instance对象,因为在指令排序的第2步操作后,instance指向的地址已经不为空。
- 解决方案:使用volatile关键字修饰instance变量
双检锁的完整实现代码如下
public class DoubleCheckLock implements Serializable {
private static volatile DoubleCheckLock instance;
/***
* 在构造方法中防止反射破坏单例
*/
private DoubleCheckLock() {
if(instance != null) {
throw new IllegalStateException();
}
}
public static DoubleCheckLock getInstance() {
if(instance == null) {
synchronized (DoubleCheckLock.class) {
if(instance == null) {
instance = new DoubleCheckLock();
}
}
}
return instance;
}
/***
* 编写一个readResolve方法防止序列化破坏单例模式
* @return 当前类的单实例对象
*/
public Object readResolve() {
return instance;
}
}