前言
juc是java并发编程的核心,里面的类有很多设计思想以及编程的技巧值得我们借鉴,个人认为,一个优秀的java程序员必须熟练掌握juc.
volatile
简介
volatile在java中是一个关键字,用于修饰类和实例变量
它确保对一个变量的更新以可以预见的方式告知其他线程。当一个域声明为volatile类型后,编译器在运行时会监视这个变量:而且对它的操作不会与其他的内存操作一起被重排序。volatile变量不会缓存在寄存器或者缓存在对其他处理器隐藏的地方。所以,读一个volatile类型的变量时,总会返回某一线程所写入的最新值
volatile可以保证变量的可见性和有序性
可见性
我们先看一个小例子
我们用两个线程共享一个变量init_value
一个线程用来读,一个线程用来写
public class SvolatileExample1 {
final static int MAX = 5;
static int init_value = 0;
public static void main(String[] args) {
new Thread(() -> {
int localValue = init_value;
while (localValue < MAX) {
if (init_value != localValue) {
System.out.printf("value was updated to [%d]\n", init_value);
localValue = init_value;
}
}
}, "Reader").start();
new Thread(() -> {
int localValue = init_value;
while (localValue < MAX) {
System.out.printf("value will update to [%d]\n", localValue++);
init_value = localValue;
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Updater").start();
}
}
输出
value will update to [0]
value will update to [1]
value will update to [2]
value will update to [3]
value will update to [4]
我们明明更新了init_value,但是读线程却获取不到这个更新。原因暂且不说,解决这个问题我们只需要在声明init_value加上volatile关键字即可
static volatile int init_value = 0;
输出
value will update to [0]
value was updated to [1]
value will update to [1]
value was updated to [2]
value will update to [2]
value was updated to [3]
value will update to [3]
value was updated to [4]
value will update to [4]
value was updated to [5]
造成这个现象的罪魁祸首就是cpu的缓存,由于现代cpu的处理速度远远大于内存的访问速度,于是cpu和内存之间加了多级缓存,来降低cpu访问内存的频率。
cpu缓存结构:
内存结构:
volatile可以保证读取的变量值是主存中的最新值
有序性
对主存的一次访问一般花费硬件的数百次时钟周期。处理器通过缓存(caching)能够从数量级上降低内存延迟的成本这些缓存为了性能重新排列待定内存操作的顺序。也就是说,程序的读写操作不一定会按照它要求处理器的顺序执行。
同样我们用一个例子来说明:
public class SvolatileExample2 {
int x = 0, y = 0;
int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
while (true) {
SvolatileExample2 svolatileExample2 = new SvolatileExample2();
Thread one = new Thread(new Runnable() {
public void run() {
svolatileExample2.a = 1;
svolatileExample2.x = svolatileExample2.b;
}
});
Thread other = new Thread(new Runnable() {
public void run() {
svolatileExample2.b = 1;
svolatileExample2.y = svolatileExample2.a;
}
});
one.start();
other.start();
one.join();
other.join();
System.out.println("x:" + svolatileExample2.x + ",y:" + svolatileExample2.y);
if (svolatileExample2.x == 0 && svolatileExample2.y == 0) break;
}
}
}
按照预期,结果可能是 x=0,y=1或者x=1,x=0或者x=1,y=1,但实际情况是结果可能为 x=0,y=0,说明在程序运行期间,发生了指令重排
同样可以通过将变量声明为volatile来解决
volatile int x = 0, y = 0;
volatile int a = 0, b = 0;
原理
内存屏障
内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。
内存屏障可以被分为以下几种类型
- LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
- StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
- LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
- StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。 在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
lock指令
从运行方面来说,加上volatile修饰词之后,汇编指令会多出一条lock…
0x00000000028ed5ed: lock add dword ptr [rsp],0h ;*putstatic a
; - svolatile.Test::main@1 (line 11)
Lock 前缀的指令在多和处理器下会引发两件事情
- 将当前处理器缓存行的数据回写到系统内存
- 这个写回的操作会使在其他cpu的缓存了该内存地址的数据无效。
在x86上的”lock …” 指令是一个Full Barrier,即StoreLoad屏障,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。Software Locks通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序。
volatile使用场景
状态标记
public class MyThread extends Thread {
private volatile boolean run = true;
@Override
public void run() {
while (run) {
//...dosomething
}
}
}
单例模式
public class LazySingleton {
private static volatile LazySingleton lazySingleton;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (lazySingleton == null) {
synchronized(LazySingleton.class) {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
}
}
return lazySingleton;
}
}
读写锁
public class Counter {
private volatile int value;
public int getValue() { return value; }
public synchronized int increment() {
return value++;
}
}