Synchronized 可见性问题
2020-07-24 本文已影响0人
leeehao
场景
一个安全的单例模式
public class CacheGetter {
private static final byte[] lock = new byte[0];
private static CacheGetter instance = null;
public CacheGetter() {
try {
Thread.sleep(200L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static CacheGetter getInstance() {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
try {
// 模拟业务
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
instance = new CacheGetter();
} else {
instance.toString();
}
}
}
return instance;
}
public static void main(String[] args) throws InterruptedException {
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 8; i++) {
threads.add(new Thread(() -> {
CacheGetter s = CacheGetter.getInstance();
System.out.println(Thread.currentThread().getName() + " " + s);
}));
}
threads.forEach(Thread::start);
Thread.sleep(1000);
}
}
输出
Thread-2
Thread-9 algorithm.CacheGetter@75672cfd
Thread-6 algorithm.CacheGetter@75672cfd
Thread-2 algorithm.CacheGetter@75672cfd
Thread-4 algorithm.CacheGetter@75672cfd
Thread-5 algorithm.CacheGetter@75672cfd
Thread-3 algorithm.CacheGetter@75672cfd
Thread-7 algorithm.CacheGetter@75672cfd
Thread-8 algorithm.CacheGetter@75672cfd
Process finished with exit code 0
问题
private static CacheGetter instance = null;
并没有 volatile
关键字修饰 synchronized 是如何保证线程间可见性呢?
JMM中关于synchronized有如下规定,线程加锁时,必须清空工作内存中共享变量的值,从而使用共享变量时需要从主内存重新读取;线程在解锁时,需要把工作内存中最新的共享变量的值写入到主存,以此来保证共享变量的可见性。(ps这里是个泛指,不是说只有在退出synchronized时才同步变量到主存)
synchronized 关键字可以保证可见性吗? - minato丶的回答 - 知乎
https://www.zhihu.com/question/48313299/answer/1166823164
被同步的线程在加锁和解锁时,必须更新工作内存。Synchronized
关键字锁的是对象可见性针对的是当前线程,下方示例两个不同的加锁方式同样保证了 instance
静态变量的可见性。
synchronized (lock) {
// do somethings
}
synchronized (CacheGetter.class) {
// do somethings
}
那么这个线程安全的单例模式安全吗?
不安全!上方代码仅保证了可见性但是并不能保证其他线程获取的是正确是实例,为什么会获取到不正确的实例,因为 CacheGetter instance = new CacheGetter()
并不是原子操作。
初始化一个实例(SomeType st = new SomeType())在java字节码中会有4个步骤,
- 申请内存空间,
- 初始化默认值(区别于构造器方法的初始化)
- 执行构造器方法
- 连接引用和实例。
这4个步骤后两个有可能会重排序,1234 1243都有可能,造成未初始化完全的对象发布。volatile可以禁止指令重排序,从而避免这个问题。作者:陈鹏
链接:https://www.zhihu.com/question/56606703/answer/149894860
来源:知乎
确保先执行构造器方法,再将引用和实例连接到一起。如果没有禁止重排序,会导致另一个线程可能获取到尚未构造完成的对象。
真正线程安全的单例
public class CacheGetter {
private static final byte[] lock = new byte[0];
private static volatile CacheGetter instance = null;
public CacheGetter() {
try {
Thread.sleep(200L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static CacheGetter getInstance() {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
try {
// 模拟业务
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
instance = new CacheGetter();
} else {
instance.toString();
}
}
}
return instance;
}
public static void main(String[] args) throws InterruptedException {
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 8; i++) {
threads.add(new Thread(() -> {
CacheGetter s = CacheGetter.getInstance();
System.out.println(Thread.currentThread().getName() + " " + s);
}));
}
threads.forEach(Thread::start);
Thread.sleep(1000);
}
}