一、简介
happen before
是时钟顺序的先后,并不能保证线程交互的可见性。
即:存在某线程对副本操作,但对于其他线程都是不可见的。
可见性:指某线程修改共享变量的指针对其他线程来说都是可见的,它反映的是指令执行的实时透明度
每个线程都有独占的内存区域,如操作栈、本地变量表等
线程本地内存保存了引用变量在堆内存中的副本,线程对变量的所有操作都在本地内存区域中进行,执行结束后再同步到堆内存中。
线程执行或线程切换都是纳秒级的。
volatile
: 挥发,不稳定。
当使用
volatile
修饰变量时,意味着任何对此变量的操作都会在内存中进行,不会产生副本,以保证共享变量的可见性,局部阻止了指令重排的发生。
volatile
解决的是多线程共享变量的可见性问题,类似于 synchronized
,但不具备sychronized
的互斥性
二、案例
(1)双检查锁(Double-checked Locking)
如下代码:
class LazyInitDemo {
private static TransactionService service = null;
public static TransactionService getTransactionService() {
if (service == null) {
// 或者 TransactionService.class
synchronized (LazyInitDemo.class) {
if (service == null) {
service == new TransactionService();
}
}
}
return service;
}
}
调用getTransactionService()
可能会得到初始化未完成的对象
原因:与 JVM 的编译优化有关
线程1 执行new TransactionService()
, 构造方法还未被调用,编译器仅仅为该对象分配了内存空间并设为默认值
线程2 调用getTransactionService()
,由于service != null
,但是此时service
对象还没有被赋予真正有效的值,从而无法取到正确的service
单例对象。
解决方法:加上volatile
private static volatile TransactionService service = null;
(2)volatile
不具互斥性
public class VolatileNotAtomic {
private static volatile long count = 0L;
private static final int NUMBER = 10000;
public static void main(String[] args) {
Thread subtractThread = new SubtractThread();
subtractThread.start();
for (int i = 0; i < NUMBER; ++i) {
count ++;
}
// 等待减法线程结束
while (subtractThread.isAlive()){}
System.out.println("count 最后的值: " + count);
}
private static class SubtractThread extends Thread {
@Override
public void run() {
for (int i = 0; i < NUMBER; ++i) {
count --;
}
}
}
}
结果基本不为0, 因为--
与++
并不是原子操作。
字节码如下:
// 1. 读取 count 并压入操作栈顶
GETSTATIC count: I
// 2. 常量 1 压入操作栈顶
ICONST_1
// 3. 取出最顶部两个元素进行相加
IADD
// 4. 将刚才得到的和赋值给 count
PUTSTATIC count: I
解决方案:针对--
与++
,可以加锁,如sychronized
三、实际场景
- 能实现
count++
原子操作的其他类有:AtomicLong
和LongAdder
JDK8 推荐使用
LongAdder
类,它比AtomicLong
性能更好,有效地减少了乐观锁的重试次数
- 一读多写的并发场景,使用
volatile
修饰变量则非常合适
最典型的应用:
CopyOnWriteArrayList