Java并发编程笔记(二)
14 Lock和Condition:
monitor in Java.util.concurrent
并发两大问题:互斥&同步
Java SDK并发包通过Lock和Condition两个接口来实现管城,其中Lock用于解决互斥问题,Condition用于解决同步问题.
与synchronized无法解决死锁造成的”破坏不可抢占条件”相比,Lock的三个接口专门解决该问题:
能够响应中断
synchronized的问题是,持有锁A后,如果尝试获取锁B失败,则线程进入阻塞状态,一旦死锁,就没有任何机会来唤醒阻塞的线程.但如果阻塞的线程能够响应中断信号从而被唤醒,那它就有机会释放锁A.
e.g.1
2
3// 支持中断的API
void lockInterruptibly()
throws InterruptedException;支持超时
如果线程超时后没有获取锁不是进入阻塞状态,而是返回一个error,那这个线程也有机会释放锁,也能破坏不可抢占条件.
e.g.1
2boolean tryLock(long time,TimeUnit unit)
throws InterruptedException;
- 非阻塞地获取锁
如果尝试获取锁失败并不进入阻塞状态而是直接返回,也有机会放锁.
e.g.1
boolean tryLock();
- 非阻塞地获取锁
Java SDK如何保证可见性
Java 里多线程的可见性是通过 Happens-Before 规则保证的,而 synchronized 之所以能够保证可见性,也是因为有一条 synchronized 相关的规则:synchronized 的解锁 Happens-Before 于后续对这个锁的加锁。那 Java SDK 里面 Lock 靠什么保证可见性呢?例如在下面的代码中,线程 T1 对 value 进行了 +=1 操作,那后续的线程 T2 就能够看到 value 的正确结果
try{}finally{}示例:
1 | class X { |
Java SDK中锁的实现简要解释
利用volatile相关的Happens-Before规则,Java SDK里的ReentrantLock内部持有一个volatile的成员变量state,获取锁或者解锁的时候会读写state的值,据相关Happens-Before规则:
- 顺序性规则:对于线程 T1,value+=1 Happens-Before 释放锁的操作 unlock();
- volatile 变量规则:由于 state = 1 会先读取 state,所以线程 T1 的 unlock() 操作 Happens-Before 线程 T2 的 lock() 操作;
- 传递性规则:线程 T1 的 value+=1 Happens-Before 线程 T2 的 lock() 操作。第二节Java内存模型中有详细解释
1
2
3
4
5
6
7
8
9
10
11
12
13class SampleLock {
volatile int state;
// 加锁
lock() {
// 省略代码无数
state = 1;
}
// 解锁
unlock() {
// 省略代码无数
state = 0;
}
}
可重入锁ReentrantLock
定义:指的是线程可以重复获取同一把锁。例如下面代码中,当线程 T1 执行到 ① 处时,已经获取到了锁 rtl ,当在 ① 处调用 get() 方法时,会在 ② 再次对锁 rtl 执行加锁操作。此时,如果锁 rtl 是可重入的,那么线程 T1 可以再次加锁成功;如果锁 rtl 是不可重入的,那么线程 T1 此时会被阻塞。如下:
1 | class X { |
公平锁与非公平锁
在使用ReentrantLock的时候,有两个构造函数,一个无参,一个传fair参数.fair为公平策略,如果传true就需要构造一个公平锁,反之则表示构造非公平锁.关于非公平锁的应用场景,在线程释放锁之后,如果来了一个线程获取锁,不必去排队直接获取到,不会入队.
1 | // unfair lock |
锁实践
Doug Lea《Java 并发编程:设计原则与模式》一书中,推荐的三个用锁的最佳实践:
- 永远只在更新对象的成员变量时加锁
- 永远只在访问可变的成员变量时加锁
- 永远不在调用其他对象的方法时加锁
总结与思考
Java SDK 并发包里的 Lock 接口里面的每个方法都是经过深思熟虑的。除了支持类似 synchronized 隐式加锁的 lock() 方法外,还支持超时、非阻塞、可中断的方式获取锁.判断下列代码是否死锁:
1 | class Account { |
- 这个会造成活锁,两个线程同时执行,线程A获取了u1.trylock,线程B获取了u2.trylock,线程A尝试获取u2.trylock,不能成功,线程A结束,同时线程B尝试获取u1.trylock,不能成功,线程B结束,又开始新一轮.A,B俩账户互相转账,各自持有自己的lock的锁,都一直尝试获取对方的锁,形成了活锁.如果在单机单核场景,活锁的可能性很小,可以接受.
改良: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
26class Account {
private int balance;
private final Lock lock
= new ReentrantLock();
// transfer
void transfer(Account tar, int amt){
boolean flag = true;
while (flag) {
if(this.lock.tryLock(随机数,NANOSECONDS)) {
try {
if (tar.lock.tryLock(随机数,NANOSECONDS)) {
try {
this.balance -= amt;
tar.balance += amt;
flag = false;
} finally {
tar.lock.unlock();
}
}//if
} finally {
this.lock.unlock();
}
}//if
}//while
}//transfer
}
15 Lock和Condition:
how Dubbo achieve transferring asynchronous to synchronous by using monitor
- condition实现了管程模型里的条件变量
Java 语言内置的管程里只有一个条件变量,而 Lock&Condition 实现的管程是支持多个条件变量的,这是二者的一个重要区别。在很多并发场景下,支持多个条件变量能够让我们的并发程序可读性更好,实现起来也更容易。例如,实现一个阻塞队列,就需要两个条件变量。