脑图
概述
上篇文章并发编程-08安全发布对象之发布与逸出中简单的描述了下对象发布和逸出的概念,并通过demo演示了不安全发布对象对象逸出(this引用逸出)。 那该如何安全的发布对象呢?
安全发布对象的4种方式
- 在静态初始化函数中初始化一个对象的引用
- 将对象的引用保存到volatile类型域或者AtomicReference对象中
- 将对象的引用保存到某个正确构造对象的final类型域中
- 将对象的引用保存到一个由锁保护的域中
示例
上面所提到的几种方法都可以应用到单例模式中,我们将以单例模式为例,介绍如何安全发布对象,以及单例实现的一些注意事项。
以前写的一篇文章: 单例模式
懒汉模式(线程不安全)
package com.artisan.example.singleton; import com.artisan.anno.NotThreadSafe; /** * 懒汉模式 单例的实例在第一次调用的时候创建 * * 单线程下没问题,多线程下getInstance方法线程不安全 * * @author yangshangwei * */ @NotThreadSafe public class SingletonLazyModel { // 私有构造函数 // 如果要保证一个类只能被初始化一次,首先要保证的是构造函数是私有的,不允许外部类直接调用new方法 private SingletonLazyModel() { // 可以初始化一些资源等 } // static单例对象 private static SingletonLazyModel instance = null; // 静态工厂方法 // public方法外部通过getInstance获取 public static SingletonLazyModel getInstance() { // 多线程情况下,假设线程A和线程B同时获取到instance为null, 这时候instance会被初始化两次 if (instance == null) { instance = new SingletonLazyModel(); } return instance; } }
饿汉模式 静态域(线程安全)
package com.artisan.example.singleton; import com.artisan.anno.ThreadSafe; /** * 饿汉模式 单例的实例在类装载的时候进行创建 * * 因为是在类装载的时候进行创建,可以确保线程安全 * * * 饿汉模式需要注意的地方: 1.私有构造函数中不要有太多的逻辑,否则初始化会慢 2.确保初始化的对象能够被使用,否则造成资源浪费 * * @author yangshangwei * */ @ThreadSafe public class SingletonHungerModel { // 私有构造函数 // 如果要保证一个类只能被初始化一次,首先要保证的是构造函数是私有的,不允许外部类直接调用new方法 private SingletonHungerModel() { // 可以初始化一些资源等 } // static单例对象 静态域 private static SingletonHungerModel instance = new SingletonHungerModel(); // public方法外部通过getInstance获取 public static SingletonHungerModel getInstance() { // 直接返回实例化后的对象 return instance; } }
改造线程不安全的懒汉模式方式一 静态方法使用synchronized修饰 (线程安全)
仅需要将静态的 getInstance方法使用synchronized修饰即可,但是缺点也很明显,线程阻塞,效率较低
synchronized修饰静态方法的作用域及demo见 并发编程-05线程安全性之原子性【锁之synchronized】
改造线程不安全的懒汉模式方式二双重检查机制(线程不安全)
改造线程不安全的懒汉模式方式一 静态方法使用synchronized修饰的缺点既然都清楚了,为了提高效率,那就把synchronized下沉到方法中的实现里吧
package com.artisan.example.singleton; import com.artisan.anno.NotThreadSafe; /** * 懒汉模式 单例的实例在第一次调用的时候创建 * * 对static getInstance方法 进行 双重检测 * * @author yangshangwei * */ @NotThreadSafe public class SingletonLazyModelOptimize2 { // 私有构造函数 // 如果要保证一个类只能被初始化一次,首先要保证的是构造函数是私有的,不允许外部类直接调用new方法 private SingletonLazyModelOptimize2() { // 可以初始化一些资源等 } // static单例对象 private static SingletonLazyModelOptimize2 instance = null; // 静态工厂方法 // public方法外部通过getInstance获取 public static SingletonLazyModelOptimize2 getInstance() { // 多线程情况下,假设线程A和线程B同时获取到instance为null, 这时候instance会被初始化两次,所以在判断中加入synchronized if (instance == null) { // synchronize修饰类 ,修饰范围是synchronized括号括起来的部分,作用于所有对象 synchronized(SingletonLazyModelOptimize2.class) { if (instance == null) { instance = new SingletonLazyModelOptimize2(); } } } return instance; } }
先说下结论: 上述代码是线程不安全的,可能会返回一个未被实例化的instance,导致错误。
这个就要从cpu的指令说起了。
问题主要出在实例化这一步
instance = new SingletonLazyModelOptimize2()
这个实例化的操作,对应底层3个步骤
memory = allocate() // 分配对象的内存空间
ctorInstance() // 初始化对象
instance = memory // 设置instance指向刚分配的内存
对于单线程,肯定是没有问题的。但是对于多线程,CPU为了执行效率,可能会发生指令重排序。
经过JVM和CPU的优化,因为第2步和第2步本质上没有先后关系,指令可能会重排成下面的顺序 1—>3—>2:
1.memory = allocate() // 分配对象的内存空间
3.instance = memory // 设置instance指向刚分配的内存
2.ctorInstance() // 初始化对象
假设按照这个指令顺序执行的话,那么当线程A执行完1和3时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行 if (instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象
改造线程不安全的懒汉模式方式二双重检查机制优化-volatile + 双重检测机制 (线程安全)
经过volatile的修饰,保证变量的可见性,当线程A执行instance = new SingletonLazyModelOptimize3的时候,JVM执行顺序会始终保证是下面的顺序:
1.memory = allocate() // 分配对象的内存空间
2.ctorInstance() // 初始化对象
3.instance = memory // 设置instance指向刚分配的内存
这样的话线程B看来,instance对象的引用要么指向null,要么指向一个初始化完毕的Instance,而不会出现某个中间态,保证了线程安全。
饿汉模式的第二种写法 静态代码块 (线程安全)
见注释
package com.artisan.example.singleton; import com.artisan.anno.ThreadSafe; /** * 饿汉模式 单例的实例在类装载的时候进行创建 * * 因为是在类装载的时候进行创建,可以确保线程安全 * * * 饿汉模式需要注意的地方: 1.私有构造函数中不要有太多的逻辑,否则初始化会慢 2.确保初始化的对象能够被使用,否则造成资源浪费 * * @author yangshangwei * */ @ThreadSafe public class SingletonHungerModel2 { // 私有构造函数 // 如果要保证一个类只能被初始化一次,首先要保证的是构造函数是私有的,不允许外部类直接调用new方法 private SingletonHungerModel2() { // 可以初始化一些资源等 } // 注意: static的顺序不要写反了,否则会抛空指针。 static的加载顺序是按顺序执行 // static单例对象 静态域 private static SingletonHungerModel2 instance = null; // 静态块 static { instance = new SingletonHungerModel2(); } // public方法外部通过getInstance获取 public static SingletonHungerModel2 getInstance() { // 直接返回实例化后的对象 return instance; } }
饿汉模式的第三种写法 静态内部类 (线程安全)
package com.artisan.example.singleton; import com.artisan.anno.ThreadSafe; /** * 饿汉模式 单例的实例在类装载的时候进行创建 * * 使用静态内部类实现的单例模式-线程安全 * * * 饿汉模式需要注意的地方: 1.私有构造函数中不要有太多的逻辑,否则初始化会慢 2.确保初始化的对象能够被使用,否则造成资源浪费 * * @author yangshangwei * */ @ThreadSafe public class SingletonHungerModel3 { // 私有构造函数 // 如果要保证一个类只能被初始化一次,首先要保证的是构造函数是私有的,不允许外部类直接调用new方法 private SingletonHungerModel3() { // 可以初始化一些资源等 } // 静态工厂方法-获取实例 public static SingletonHungerModel3 getInstance() { // 直接返回实例化后的对象 return InstanceHolder.INSTANCE; } // 用静态内部类创建单例对象 private 修饰 private static class InstanceHolder { private static final SingletonHungerModel3 INSTANCE = new SingletonHungerModel3(); } }
注意事项
从外部无法访问静态内部类InstanceHolder (private修饰的),只有当调用Singleton.getInstance方法的时候,才能得到单例对象instance。
instance对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类InstanceHolder 被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。
小结
小结: 以上所提到的单例实现方式并不能算是完全安全的,这里的安全不仅指线程安全还有发布对象的安全。因为以上例子所实现的单例模式,我们都可以通过反射机制去获取私有构造器更改其访问级别从而实例化多个不同的对象。
那么如何防止利用反射构建对象呢?这时我们就需要使用到内部枚举类了,因为JVM可以阻止反射获取枚举类的私有构造方法。
枚举模式 推荐 ( 线程安全,防止反射构建)
package com.artisan.example.singleton; import lombok.Getter; public class SingletonEum { /** * 私有构造函数 */ private SingletonEum() { } /** * 静态工厂方法-获取实例 * * @return instance */ public static SingletonEum getInstance() { return Singleton.INSTANCE.getInstance(); } /** * 由枚举类创建单例对象 */ @Getter private enum Singleton { INSTANCE; /** * 单例对象 */ private SingletonEum instance; /** * JVM保证这个方法绝对只调用一次 */ Singleton() { instance = new SingletonEum(); } } }
使用枚举实现的单例模式,不但可以防止利用反射强行构建单例对象,而且可以保证线程安全,并且可以在枚举类对象被反序列化的时候,保证反序列的返回结果是同一对象。
上面代码中之所以使用内部枚举类的原因是为了让这个单例对象可以懒加载,相当于是结合了静态内部类的实现思想。若不使用内部枚举类的话,单例对象就会在枚举类被加载的时候被构建。