一.简介
Java SDK并发包通过Lock和Condition两个接口来实现管程,其中Lock用于解决互斥问题,Condition 用于解决同步问题。
二.原理
2.1 管程
在 Java 的 1.5 版本中,synchronized 性能不如 SDK 里面的 Lock,但 1.6 版本之后,synchronized 做了很多优化,将性能追了上来,所以 1.6 之后的版本又有人推荐使用 synchronized 了。
但是synchronized 自动加锁和解锁,无法解决破坏无可抢占的方案,基于这种情况Lock 应运而生,有三种方案:
- 能够响应中断。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。
- 支持超时。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
- 非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
// 支持中断的API
void lockInterruptibly()
throws InterruptedException;
// 支持超时的API
boolean tryLock(long time, TimeUnit unit)
throws InterruptedException;
// 支持非阻塞获取锁的API
boolean tryLock();
2.2 可见性
利用volatile相关的Happens-Before规则,JavaSDK里面的ReentrantLock,内部持有一个 volatile 的成员变量 state,获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state 的值(简化后的代码如下面所示)。也就是说,在执行 value+=1 之前,程序先读写了一次 volatile 变量 state,在执行 value+=1 之后,又读写了一次 volatile 变量 state。
class SampleLock {
volatile int state;
// 加锁
lock() {
// 省略代码无数
state = 1;
}
// 解锁
unlock() {
// 省略代码无数
state = 0;
}
}
- 顺序性规则,对于线程T1,value+=1 Happens-Before释放锁的操作unlock();
- volatile变量规则,由于state = 1会先读取state,所以线程T1的unlock()操作Happens-Before 线程 T2 的 lock() 操作;
- 传递性规则,线程T1的value+=1 Happens-Before 线程 T2 的 lock() 操作。
2.3 可重入锁
可重入锁的意思,指的是线程可以重复获取同一把锁。
class X {
private final Lock rtl =
new ReentrantLock();
int value;
public int get() {
// 获取锁
rtl.lock();
try {
return value;
} finally {
// 保证锁能释放
rtl.unlock();
}
}
public void addOne() {
// 获取锁
rtl.lock();
try {
value = 1 + get();
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
2.4 公平锁与非公平锁
在使用 ReentrantLock 的时候,你会发现 ReentrantLock 这个类有两个构造函数,一个是无参构造函数,一个是传入 fair 参数的构造函数。fair 参数代表的是锁的公平策略,如果传入 true 就表示需要构造一个公平锁,反之则表示要构造一个非公平锁。
//无参构造函数:默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//根据公平策略参数创建锁
public ReentrantLock(boolean fair){
sync = fair ? new FairSync()
: new NonfairSync();
}
公平锁,唤醒的策略就是谁等待的时间长,就唤醒谁,很公平;如果是非公平锁,则不提供这个公平保证,有可能等待时间短的线程反而先被唤醒。
2.5 用锁的最佳实践
并发大师 Doug Lea《Java 并发编程:设计原则与模式》
- 永远只在更新对象的成员变量时加锁
- 永远只在访问可变的成员变量时加锁
- 永远不在调用其他对象的方法时加锁
2.6 示例
生成消费模型,同步器。
public class AQSLockTest{
final static Queue<String> queue = new LinkedBlockingQueue<>();
final static Lock lock = new ReentrantLock();
final static Condition full = lock.newCondition();
final static Condition empty = lock.newCondition();
final static int QUEUE_MAX_SIZE = 3;
public static void add() throws InterruptedException {
lock.lock();
try {
//队列满了
while (queue.size() == QUEUE_MAX_SIZE){
full.await();
}
System.out.println("prd:" + "hello");
queue.add("test1-"+new Random().nextInt(100));
empty.signalAll();
}finally {
lock.unlock();
}
}
public static void poll() throws InterruptedException {
lock.lock();
try {
// 当队列queue中一个字符串都没有,就将剩下的消费线程丢进enpty对应的队列中
while (queue.size() == 0) {
empty.await();
}
// 消费队列queue中的字符串
String poll = queue.poll();
System.out.println("consumer:" + poll);
// 消费成功,就唤醒full中所有的生产线程去生产字符串
full.signalAll();
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
// 生产者线程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
// 消费者线程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
poll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
参考
《Java并发编程实战》
公众号
微信公众号(bigdata_limeng)