Java并发编程的艺术 学习笔记(2)

Java并发机制的底层实现原理

在多线程并发编程中,volatile和synchronized都扮演者重要角色,volatile是轻量级锁,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改了一个共享变量时,另一个线程能够读到这个修改的值。

如果volatile运用得当,它比synchronized的的使用和执行成本更低,因为它不会引起线程的上下文切换和调度。

volatile

Java语言提供volatile,如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

实现原理

有volatile变量修饰的共享变量进行写操作的时候会多出一行汇编代码。

1
2
3
4
5
instance = new Instance(); //instance是volatile变量
//转成汇编代码如下
0x01a3de1d: movb $0x0, 0x1104800(%esi);
0x01a3de24: lock addl $0x0, (%esp);

Lock前缀指令在多核处理器下会引发两件事情。

a. 将当前处理器的缓存行数据写回系统的内存

b. 这个写回内存的操作,会使其它CPU里缓存了该内存地址的数据无效

synchronized

重量级锁synchronized实现同步的基础:Java中每一个对象都可以作为锁。

a. 对于普通同步方法,锁是当前的实例对象

b. 对于静态同步方法,锁是当前类的Class对象

c. 对于同步方法块,锁是Synchonized括号里配置的对象

原子操作的实现原理

比较并交换(CAS):CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较旧值有没有发生变化,如果没有发生变化,才交换成新值,否则不交换。

处理器实现原子操作

a. 通过总线锁保证原子性

b. 使用缓存锁保证原子性

总线锁把CPU和内存之间的通信锁住了,其他处理器不能操作其他的内存地址的数据,开销较大。

用循环CAS实现原子操作

在Java中可以使用锁和循环CAS实现原子操作

JVM中的CAS操作就是利用了处理器提供的CMPXCHG指令实现。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。

使用自旋CAS实现的原子操作

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.myConcurrent.Chapter2;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class Automic {
private AtomicInteger atomicInteger = new AtomicInteger(0);
private int i = 0;
//非线程安全的计数器
private void unSafeCount(){
++i;
}
//使用CAS实现线程安全的计数器
private void safeCount(){
for( ; ; ){
int old = atomicInteger.get();
boolean suc = atomicInteger.compareAndSet(old, ++i);
if(suc){
break;
}
}
}
public static void main(String []args){
final Automic automic = new Automic();
List<Thread> threadList = new ArrayList<>(600);
long start = System.currentTimeMillis();
for(int j = 0; j < 100; ++j){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 1000000; ++i){
automic.unSafeCount();
automic.safeCount();
}
}
});
threadList.add(t);
}
for(Thread t: threadList){
t.start();
}
//等待所有线程执行完成
for(Thread t: threadList){
try{
t.join();
}catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.println("unSafe: " + automic.i);
System.out.println(" Safe: " + automic.atomicInteger.get());
System.out.println("time(ms): " + String.valueOf(System.currentTimeMillis() - start));
}
}

CAS实现原子操作的三大问题

a. ABA问题。从JDK1.5开始,提供AtomicStampedReference来解决问题。这个类的CompareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查版本号是否等于预期版本。

1
2
3
4
5
6
public boolean compareAndSet(
V expectedReference,
V newReference,
int expectedStamp,
int newStamp
)

b. 循环时间长,开销大

c. 只能保证一个共享变量的原子操作。从JDK1.5开始,提供AtomicReference类来保证引用对象之间的原子性,就可以把多个变量塞进一个对象进行原子操作。

比较

优点 缺点 适用场景
轻量级锁 提高线程响应速度 会空旋,消耗CPU 同步块执行速度快
重量级锁 提高吞吐量 线程阻塞 同步块执行速度慢