Java volatile 与 CAS 的区别

Introduction

锁的特性

锁具有两种主要特性:互斥可见性

  • 互斥指的是一次只允许一个线程持有某个特定的锁,因此可以保证共享数据内容的一致性;
  • 可见性指的是必须确保锁被释放之前对共享数据的修改,随后获得锁的另一个线程能够知道该行为。

锁的代价

锁是用来做并发最简单的方式,当然其代价也是最高的。内核态的锁的时候需要操作系统进行一次上下文切换,加锁、释放锁会导致比较多的上下文切换和调度延时,等待锁的线程会被挂起直至锁释放。在上下文切换的时候,cpu之前缓存的指令和数据都将失效,对性能有很大的损失。操作系统对多线程的锁进行判断就像两姐妹在为一个玩具在争吵,然后操作系统就是能决定他们谁能拿到玩具的父母,这是很慢的。用户态的锁虽然避免了这些问题,但是其实它们只是在没有真实的竞争时才有效。

Java在JDK1.5之前都是靠synchronized关键字保证同步的,这种通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有守护变量的锁,都采用独占的方式来访问这些变量,如果出现多个线程同时访问锁,那第一些线线程将被挂起,当线程恢复执行时,必须等待其它线程执行完他们的时间片以后才能被调度执行,在挂起和恢复执行过程中存在着很大的开销。锁还存在着其它一些缺点,当一个线程正在等待锁时,它不能做任何事。如果一个线程在持有锁的情况下被延迟执行,那么所有需要这个锁的线程都无法执行下去。如果被阻塞的线程优先级高,而持有锁的线程优先级低,将会导致优先级反转(Priority Inversion)。

乐观锁和悲观锁

独占锁是一种悲观锁,synchronized就是一种独占锁,它假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

volatile

与锁相比,volatile变量是一和更轻量级的同步机制,因为在使用这些变量时不会发生上下文切换和线程调度等操作,但是volatile变量也存在一些局限:不能用于构建原子的复合操作,因此当一个变量依赖旧值时就不能使用volatile变量。

所有读值操作都执行一个包含获取语义的读取操作,这些方法读取由参数address引用的值,然后使得CPU高速缓存内的相应字节失效;所有写值操作都执行一个包含释放语义的写入操作,这些方法将CPU高速缓存内的字节刷到内存中,然后将address参数引用的值修改为value参数所表示的值。

volatile变量具有”lock”的可见性,却不具备原子特性。也就是说线程能够自动发现volatile变量的最新值。volatile变量可以实现线程安全,但其应用有限。使用volatile变量的主要原因在于它使用非常简单,至少比使用锁机制要简单的多;其次便是性能原因了,某些情况下,它的性能要优于锁机制。此外,volatile操作不会造成阻塞。

使用volatile来确保线程安全的前提条件:

  • 对变量的写操作不依赖于当前值;
  • 该变量没有包含在具有其他变量的不变式中。

Atomic Operations

  • all assignments of primitive types except for long and double
  • all assignments of references
  • all operations of java.concurrent.Atomic* classes
  • all assignments to volatile longs and doubles

常见的i++操作

如果是非原子性操作,那么i++将分为三步完成:

  • 读取当前i的值;
  • 对i进行自增;
  • 写入i最新的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Counter {
private var count = 0

fun inc() {
count += 1
}

fun dec() {
count -= 1
}

fun getValue(): Int {
return this.count
}
}

假设有两个线程分别执行 inc() 和 dec() 操作,两个线程操作时序如下:

  1. Thread A 读取到 count 值为 0;
  2. Thread B 读取到 count 值为 0;
  3. Thread A inc(),count变成了 1;
  4. Thread B dec(),变成了 -1;
  5. Thread A 更新 count 的值为 1;
  6. Thread B 更新 count 的值为 -1;

执行的结果就是 Thread B 的写入操作覆盖了 Thread A的值。

为什么long型赋值不是原子操作呢?

1
long foo = 65465498L;

因为在实际中,Java会分两步写入这个long变量,先写32位,再写后32位。这样就线程不安全了。如果改成下面的就线程安全了:

1
2
//  volatile内部已经做了synchronized
private volatile long foo;

volatile不能保证原子性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class TestVolatileAtomic {
@Volatile
var count = 0

fun inc() {
count += 1
}
}

fun main() {
val threadCount = 100
val volatileAtomic = TestVolatileAtomic()
var runningTimes = 0
var errorTimes = 0

while (true) {
val countDownLatch = CountDownLatch(threadCount)
val startup = CountDownLatch(threadCount)

for (index in 0 until threadCount) {
thread {
startup.countDown()
// println("threadId ${Thread.currentThread().id} waiting")
startup.await()
volatileAtomic.inc()
countDownLatch.countDown()
// println("threadId ${Thread.currentThread().id} finish")
}
}

countDownLatch.await()
if (volatileAtomic.count % threadCount != 0) {
println("count ${volatileAtomic.count}")
errorTimes += 1
}
volatileAtomic.count = 0
runningTimes += 1
println("runningTimes $runningTimes, errorTimes=$errorTimes")
}
}

测试输出结果:

1
2
3
......
runningTimes 1128, errorTimes=9
runningTimes 1129, errorTimes=9

Synchronized 确保”原子性”

1
2
3
@Synchronized fun inc() {
count += 1
}

Atomic Class 确保”原子性”

1
2
3
4
5
6
7
8
class TestVolatileAtomic {
// @Volatile
var count = AtomicInteger(0)

fun inc() {
count.incrementAndGet()
}
}

Reference