1️⃣发布与逸出
① 概念
发布对象 : 使一个对象能够被当前范围之外的代码所使用;
对象逸出 : 一种错误的发布,当一个对象还没有构造完成时,就使他被其他线程所见;
② 代码演示
/**
* 发布对象
*/
@Slf4j
@NotThreadSafe
public class UnsafePublish {
private String[] states = {"a", "b", "c"};
public String[] getStates() {
return states;
}
public static void main(String[] args) {
UnsafePublish unsafePublish = new UnsafePublish();
log.info("{}", Arrays.toString(unsafePublish.getStates()));
unsafePublish.getStates()[0] = "d";
log.info("{}", Arrays.toString(unsafePublish.getStates()));
}
}
从结果可以看出这个类是不安全的,因为我们无法确认别的线程是否对这个对象进行修改;
/**
* 对象逸出
*/
@Slf4j
@NotThreadSafe
@NotRecommend
public class Escape {
private int thisCanBeEscape = 0;
public Escape () {
new InnerClass();
}
private class InnerClass {
public InnerClass() {
log.info("{}", Escape.this.thisCanBeEscape);
}
}
public static void main(String[] args) {
new Escape();
}
}
这个也是一个线程不安全的,这会导致发布线程以外的其他线程会看到过期的值;
2️⃣安全发布对象的四种方法
① 在静态初始化函数中初始化一个对象引用;
② 将对象的引用保存到volatile类型域或者AtomicReference对象中;
③ 将对象的引用保存到某个正确构造对象的final类型域中;
④ 将对象的引用保存到一个由锁保护的域中;
/**
* 懒汉模式
* 单例实例在第一次使用时进行创建
*/
@NotThreadSafe
public class SingletonExample1 {
// 私有构造函数
private SingletonExample1() { }
// 单例对象
private static SingletonExample1 instance = null;
// 静态的工厂方法
public static SingletonExample1 getInstance() {
if (instance == null) {
instance = new SingletonExample1();
}
return instance;
}
}
上边的实现在单线程中没有问题,因为我们在对象创建之前进行了判断,如果在多线程环境下就会出现问题,这个问题是由于多个线程同时获取到了不同的初始化对象导致;
/**
* 饿汉模式
* 单例实例在类装载时进行创建
*/
@ThreadSafe
public class SingletonExample2 {
// 私有构造函数
private SingletonExample2() { }
// 单例对象
private static SingletonExample2 instance = new SingletonExample2();
// 静态的工厂方法
public static SingletonExample2 getInstance() {
return instance;
}
}
这个类是线程安全的,因为我们使用了单例模式的饿汉式在类第一次被装载的时候就会创建对象且因为是静态的又只会被创建一次,所以他是线程安全的;但是这个也是有缺点的,如果初始化的时候执行过多的操作会导致加载速度特别慢导致性能的问题,如果只进行资源的加载而没有调用的话又会导致资源的浪费;
/**
* 懒汉模式
* 单例实例在第一次使用时进行创建
*/
@ThreadSafe
@NotRecommend
public class SingletonExample3 {
// 私有构造函数
private SingletonExample3() {
}
// 单例对象
private static SingletonExample3 instance = null;
// 静态的工厂方法
public static synchronized SingletonExample3 getInstance() {
if (instance == null) {
instance = new SingletonExample3();
}
return instance;
}
}
获取对象的方法经过synchronized的修饰就会出现在同一个时间段内只能有一个线程进行访问,所以懒汉式也将会变成线程安全的;但是这样的写法我们并不推荐,因为加了synchronized虽然保证了线程安全但是却带来了性能上的开销;
/**
* 懒汉模式 -》 双重同步锁单例模式
* 单例实例在第一次使用时进行创建
*/
@NotThreadSafe
public class SingletonExample4 {
// 私有构造函数
private SingletonExample4() { }
// 单例对象
private static SingletonExample4 instance = null;
// 静态的工厂方法
public static SingletonExample4 getInstance() {
if (instance == null) { // 双重检测机制 // B
synchronized (SingletonExample4.class) { // 同步锁
if (instance == null) {
instance = new SingletonExample4(); // A - 3
}
}
}
return instance;
}
}
但是这个类也不是线程安全的,当我们执行到这一行代码instance = new SingletonExample4();的时候;他会进行以下三步的操作:
1、memory = allocate() 分配对象的内存空间
2、ctorInstance() 初始化对象
3、instance = memory 设置instance指向刚分配的内存
在完成这三步以后我们的instance就只想实际分配的内存地址了;在单线程的情况是没有什么问题的但是在多线程情况下就会出现以下的情况:
JVM和cpu优化,发生了指令重排
1、memory = allocate() 分配对象的内存空间
3、instance = memory 设置instance指向刚分配的内存
2、ctorInstance() 初始化对象
当发生了指令重排序以后,这个类就会变成线程不安全的了;
/**
* 懒汉模式 -》 双重同步锁单例模式
* 单例实例在第一次使用时进行创建
*/
@ThreadSafe
public class SingletonExample5 {
// 私有构造函数
private SingletonExample5() {
}
// 1、memory = allocate() 分配对象的内存空间
// 2、ctorInstance() 初始化对象
// 3、instance = memory 设置instance指向刚分配的内存
// 单例对象 volatile + 双重检测机制 -> 禁止指令重排
private volatile static SingletonExample5 instance = null;
// 静态的工厂方法
public static SingletonExample5 getInstance() {
if (instance == null) { // 双重检测机制 // B
synchronized (SingletonExample5.class) { // 同步锁
if (instance == null) {
instance = new SingletonExample5(); // A - 3
}
}
}
return instance;
}
}
因为前一个例子发生了指令重排序导致了线程不安全,那么我们通过volatile关键字限制指令重排序这样就会变成线程安全的了;这就是volatile的双重检测使用场景;关于懒汉模式我们就先分析到这里,接下来我们看一下恶汉模式
/**
* 饿汉模式
* 单例实例在类装载时进行创建
*/
@ThreadSafe
public class SingletonExample6 {
// 私有构造函数
private SingletonExample6() { }
// 单例对象
private static SingletonExample6 instance = null;
static {
instance = new SingletonExample6();
}
// 静态的工厂方法
public static SingletonExample6 getInstance() {
return instance;
}
public static void main(String[] args) {
System.out.println(getInstance().hashCode());
System.out.println(getInstance().hashCode());
}
}
当我们在写静态域或者静态代码块的时候一定要注意书写顺序否则会出现NPE;
/**
* 枚举模式:最安全
*/
@ThreadSafe
@Recommend
public class SingletonExample7 {
// 私有构造函数
private SingletonExample7() { }
public static SingletonExample7 getInstance() {
return Singleton.INSTANCE.getInstance();
}
private enum Singleton {
INSTANCE;
private SingletonExample7 singleton;
// JVM保证这个方法绝对只调用一次
Singleton() {
singleton = new SingletonExample7();
}
public SingletonExample7 getInstance() {
return singleton;
}
}
}
当我们通过枚举来初始化这个对象的时候,它可以保证这个方法绝对只会被执行一次且是在这个类调用之前初始化的,因此这个类是线程绝对安全的,推荐使用这种方式,因为这种方式比懒汉式更安全,比饿汉式更加节省资源;