[并发] 5 安全发布对象
1.不安全的发布对象
1.1
@Slf4j
public class UnsafePublish {
private String[] states = {"a", "b", "c"};
//通过public供外界访问states,这实际上是不安全的,其他线程会修改这个域
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()));
}
}
这里的UnsafePublish是不安全的发布对象,因为外界可以访问到states,并且可以修改states,因此是不安全的。
1.2
@Slf4j
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.安全的发布对象
<1> 在静态初始化函数中初始化一个对象引用
<2> 将对象的引用保存到volatile类型域或者AtomicReference对象
<3> 将对象的引用保存到某个正确构造对象的final类型域中
<4> 将对象的引用保存到一个由锁保护的域中
上述方法的应用:
(1)很熟悉的懒汉单例模式:在单例实例第一次使用时进行创建
/**
* 懒汉模式
*/
public class SingletonExample1 {
//构造方法私有化
private SingletonExample1() {
}
//单例对象
private static SingletonExample1 instance = null;
//静态的工程方法
public static SingletonExample1 getInstance() {
if (instance == null) {
instance = new SingletonExample1();
}
return instance;
}
}
但是在多线程环境下,该懒汉单例模式可能会出现问题:
如果两个线程同时判断instance为null,那么两个线程就会同时实例化 new SingletonExample1(),即实例化了两次,导致线程不安全。
(2)饿汉模式
/**
* 饿汉模式
* 在类装载使用时进行创建
*/
@ThreadSafe
public class SingletonExample2 {
//构造方法私有化
private SingletonExample2() {
}
//单例对象
private static SingletonExample2 instance = new SingletonExample2();
//静态的工程方法
public static SingletonExample2 getInstance() {
return instance;
}
}
饿汉模式不存在线程不安全问题。但是如果这个类在初始化的时候有非常多的处理,就会导致这个类装载特别慢,引起性能问题或者资源浪费。
饿汉模式的另一种写法:
@ThreadSafe
public class SingletonExample5 {
//构造方法私有化
private SingletonExample5() {
}
//单例对象
private static SingletonExample5 instance = null;
static {
instance=new SingletonExample5();
}
//静态的工程方法
public static SingletonExample5 getInstance() {
return instance;
}
}
注意顺序,静态代码块要在声明域后面。
(3)懒汉模式改造====>线程安全
a做法:
只要将getInstance()方法,加上synchronized关键字,那么同一时刻只允许一个线程调用getInstance()方法。
但是该方法并不推荐,因为synchronized会带来性能上的开销。
b做法:双重同步锁单例模式
@NotThreadSafe
public class SingletonExample3 {
//构造方法私有化
private SingletonExample3() {
}
//单例对象
private static SingletonExample3 instance = null;
//静态的工程方法
public static SingletonExample3 getInstance() {
if (instance == null) {
synchronized (SingletonExample3.class) {
if (instance == null) {
instance = new SingletonExample3();
}
}
}
return instance;
}
}
在方法内,加上synchronized关键字,修饰代码块,
进行双重检测机制。
但是该方法还是线程不安全的,
1.memory=allocate(),分配对象的内存空间
2.初始化对象
3.instance=memory 设置instance指向刚分配的内存
在多线程下,上面3个指令可能因为JVM和cpu优化,会发生指令重排,按照1,3,2的顺序执行。
所以解决办法是private static SingletonExample3 instance = null;
加上volatile,限制指令重排,即
private volatile static SingletonExample3 instance = null;
(4)静态内部类模式
public class SingletonObject6 {
private SingletonObject6(){
}
// 单例持有者
private static class InstanceHolder{
private final static SingletonObject6 instance = new SingletonObject6();
}
//
public static SingletonObject6 getInstance(){
// 调用内部类属性
return InstanceHolder.instance;
}
}
该模式下,实例由内部类创建,由于JVM在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会加载,并初始化其静态属性。
静态属性由static修饰,保证只被实例化一次,并且严格保证实例化顺序。
(5)枚举类实现单例模式
@ThreadSafe
@Recommend
public class SingletonExample6 {
private SingletonExample6() {
}
public static SingletonExample6 getInstance() {
return Singleton.INSTANCE.getInstance();
}
private enum Singleton {
INSTANCE;
private SingletonExample6 singleton;
//JVM保证这个方法绝对只调用一次
Singleton() {
singleton = new SingletonExample6();
}
public SingletonExample6 getInstance() {
return singleton;
}
}
}
因为枚举类型是线程安全的,并且只会被装载一次,并且枚举类型是所有单例实现中唯一一种不会被破坏的单例实现模式。