学习java同步机制 (一) 基础篇
本篇将介绍java并发中的几个非常重要的基础概念,后面更高级的工具均建立在它们的基础之上。
volatile关键字
volatile是java中的一个关键字,包含两种语义:
可见性: 即当一个共享变量别一个线程修改时,保证其他线程可以读取到修改后的值而不是修改前的值。其内部原理为:
在java的内存模型中,一个共享变量会存储在主存(Main Memory)中, 而当多个线程对它进行操作时每一个线程的本地内存(Local Memory)均会维护一个该变量的副本。不加volatitle关键字时线程读取该变量会优先从本地内存读取,不存在时才从主存读取;写变量时优先写到本地内存中,根据一定的策略决定某个时刻回写到主存中。此过程可类比CPU从主存读取数据和缓存数据的过程。然而此时,如果一个线程修改了变量,在没有回写之前,另一个线程看到的依然是其本地内存中的副本,但显然这个副本这个时候已经是一个旧的值了。为了解决这个问题,可以为变量添加volatile关键字,它会使得变量被更新时会回写到主存中,同时强制其他线程的本地内存中该变量的副本失效,使得其他线程在读的时候需要重新去主存中读取。这样便达到了变量被一个线程修改后其他线程都可以看到更新后的值,即所谓的
对其他线程的可见性
。内存屏障: 保证操作volatile变量的指令不会被CPU重排序。
两条没有依赖关系的指令(如: int a = b; int c = d;)在最终执行的时候,其顺序可能会与定义不一致,这是CPU为了提高指令执行速度而做的优化操作。
然而,在并发环境中,这种顺序的变动可能会动我们的业务逻辑产生破坏。volatile关键字可为该变量的操作指令添加内存屏障,以禁止这种指令重排序。
CAS操作
CAS,即Compare And Swap,其基础语义为:
更新一个变量的值,如果我们修改它的时候它还是我们期望的值,即没有被别的线程修改过,那么修改成功,否则失败。
CAS操作用于并发环境中对一个共享变量的修改。其思想类似于数据库操作中的乐观锁。最终由硬件层的CPU指令提供支持。在Java中CAS操作由Unsafe类的compareAndSwapXxx方法提供支持:
boolean compareAndSwapObject(object, offset, expect, update);
第一个参数和第二个参数决定了该变量的内存地址。第三个参数是我们期望的该变量的值,第四个参数是我们想把它修改成的值。
注意:
- CAS操作需要与volatile配合使用。
- 如果使用compareAndSwapObject,除object本身外,对象内部的属性也应该用volatile修饰。
AtomicXxxx系列
基于CAS,JDK为我们封装了一系列基础数据类型和引用类型的线程安全的操作类。主要有AtomicInteger, AtomicLong, AtomicReference等。为我们在多线程环境下安全地操作变量提供了便利。其使用可参考如下示例代码:
public static AtomicInteger num = new AtomicInteger(0);
@Test
public void testCustomIncreasement() throws InterruptedException {
int concurency = 10;
final CountDownLatch latch = new CountDownLatch(concurency);
for (int i = 0; i < concurency; i++) {
Thread increaseThread = new Thread() {
@Override
public void run() {
for (; ; ) {
int oldValue = num.get();
System.out.println("取出来的值为:" + oldValue);
int newValue = doBusiness(oldValue);
System.out.println("业务处理后的值为:" + newValue);
//CAS
if (num.compareAndSet(oldValue, newValue)) {
break;
} else {
System.out.println("检测到冲突,有别的线程修改");
}
}
latch.countDown();
}
};
increaseThread.start();
}
latch.await();
//同步正确则为10,否则小于10
System.out.println(num);
Assert.assertEquals(concurency, num.get());
}