并发编程的两个核心问题:互斥,同个时刻只有有一个线程;同步,线程之间如何通信,协作。Lock和Conditon两个接口实现管程,其中Lock实现互斥,Condition实现同步。
java已经有synchronized实现管程,为什么要重复造轮子呢?他们有什么区别?
1. 再造管程的理由
解决死锁的方案之一是:破坏不可抢占的条件,占有资源的线程申请其他资源时,申请不到,就主动释放占有的资源。
三种方案:
- 能够响应中断。阻塞状态的线程能够响应中断信号,唤醒它,就能够释放锁;
- 支持超时。线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。
- 非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。
体现在Lock上,三个接口:
// 支持中断的 API
void lockInterruptibly() throws InterruptedException;
// 支持超时的 API
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 支持非阻塞获取锁的 API
boolean tryLock();
2. 如何保证可见性
Java SDK 里面 Lock 的使用,经典的范例,try{}finally{}。
下列代码,对 value 进行了 +=1 操作,后续的线程 T2 能够看到 value 的正确结果。
class X {
private final Lock rtl = new ReentrantLock();
int value;
public void addOne() {
// 获取锁
rtl.lock();
try {
value += 1;
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
原理: volatile 相关的 Happens-Before 规则,指一个volatile变量的写操作,Happens-Before后续对这个变量的读操作。
Java SDK 里面的 ReentrantLock,内部持有一个 volatile 的成员变量 state,获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state 的值(简化后的代码如下面所示)。也就是说,在执行 value+=1 之前,程序先读写了一次 volatile 变量 state,在执行 value+=1 之后,又读写了一次 volatile 变量 state。根据相关的 Happens-Before 规则:
- 顺序性规则:对于线程 T1,value+=1 Happens-Before 释放锁的操作 unlock();
- volatile 变量规则:由于 state = 1和state = 0 会先读取 state,所以线程 T1 的 unlock() 操作 Happens-Before 线程 T2 的 lock() 操作;
- 传递性规则:线程 T1 的 value+=1 操作 Happens-Before 线程 T12的 lock()。
class SampleLock {
volatile int state;
// 加锁
lock() {
// 省略代码无数
state = 1;
}
// 解锁
unlock() {
// 省略代码无数
state = 0;
}
}
3. 什么是可重入锁
顾名思义, 线程可以重复获取一把锁。
class X {
private final Lock rtl = new ReentrantLock();
int value;
public int get() {
// 获取锁
rtl.lock(); // (3)
try {
return value;
} finally {
// 保证锁能释放
rtl.unlock();
}
}
public void addOne() {
// 获取锁
rtl.lock(); // (1)
try {
value = 1 + get(); // (2)
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
当线程执行addOne()方法时,执行到(2)前,已经在(1)获取锁了,此时(2)执行get()方法将再次获取锁,如果锁是不可重入的,线程将阻塞。
可重入函数,指的是多个线程可以同时调用该函数。
4. 公平锁和非公平锁
ReentrantLock有两个构造函数。有参构造函数传入true参数就是公平锁。
// 无参构造函数:默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 根据公平策略参数创建锁
public ReentrantLock(boolean fair){
sync = fair ? new FairSync() : new NonfairSync();
}
在8.管程:并发编程的万能钥匙 - 理论基础介绍过入口等待队列,一个锁对应一个等待队列。公平锁,哪个线程的等待时间长就唤醒谁;非公平锁,不一定。
5. 用锁的最佳实践
Doug Lea《Java 并发编程:设计原则与模式》,书中介绍:
- 永远只在更新对象的成员变量时加锁
- 永远只在访问可变的成员变量时加锁
- 永远不在调用其他对象的方法时加锁