一、Lock 和 Condition
Java 并发包中的 Lock 和 Condition 主要解决的是线程的互斥和同步问题,这两者的配合使用,相当于 synchronized、wait()、notify() 的使用。
1. Lock 的优势
比起传统的 synchronized 关键字,Lock 最大的不同(或者说优势)在于:
- 阻塞的线程能够响应中断,这样能够有机会释放自己持有的锁,避免死锁
- 支持超时,如果线程在一定时间内未获取到锁,不是进入阻塞状态,而是抛出异常
- 非阻塞的获取锁,如果未获取到锁,不进入阻塞状态,而是直接返回
三种情况分别对应 Lock 的三个方法:void lockInterruptibly()
,boolean tryLock(long time, TimeUnit unit)
,boolean tryLock()
。
Lock 最常用的一个实现类是 ReentrantLock,代表可重入锁,意思是可以反复获取同一把锁。
除此之外,Lock 的构造方法可以传入一个 boolean 值,表示是否是公平锁。
2. Lock 和 Condition 的使用
前面实现的简单的阻塞队列就是使用 Lock 和 Condition ,现在其含义已经非常明确了:
public class BlockingQueue<T> { private int capacity; private int size; //定义锁和条件 private final Lock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); /** * 入队列 */ public void enqueue(T data){ lock.lock(); try { //如果队列满了,需要等待,直到队列不满 while (size >= capacity){ notFull.await(); } //入队代码,省略 //入队之后,通知队列已经不为空了 notEmpty.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { //在finally块中释放锁,避免死锁 lock.unlock(); } } /** * 出队列 */ public T dequeue(){ lock.lock(); try { //如果队列为空,需要等待,直到队列不为空 while (size <= 0){ notEmpty.await(); } //出队代码,省略 //出队列之后,通知队列已经不满了 notFull.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } //实际应该返回出队数据 return null; } }
可以看到,Lock 需要手动的加锁和解锁,并且解锁操作是放在 finally 块中的,这是一种编程范式,尽量遵守。
二、ReadWriteLock
ReadWriteLock 表示读写锁,适用于读多写少的情况,读写锁一般有几个特征:
- 读锁与读锁之间不互斥,即允许多个线程同时读变量。
- 写锁与读锁之间互斥,一个线程在写时,不允许读操作。
- 写锁与写锁之间互斥,只允许 一个线程写操作。
读写锁减小了锁的粒度,在读多写少的场景下,对性能的提升较为明显。ReadWriteLock 的简单使用示例如下:
public class ReadWriteLockTest { private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock readLock =lock.readLock(); private final Lock writeLock =lock.writeLock(); private int value; //加写锁 private void addValue(){ writeLock.lock(); try { value += 1; } finally { writeLock.unlock(); } } //加读锁 private int getValue(){ readLock.lock(); try { return value; } finally { readLock.unlock(); } } }
读写锁的升级与降级
Java 中不允许锁的升级,即加写锁时必须释放读锁。
但是允许锁的降级,即加读锁时,可以不释放写锁,最后读锁和写锁一起释放。
三、StampedLock
1. StampedLock 的使用及特点
StampedLock 是 Java 1.8 版本中提供的锁,主要支持三种锁模式:写锁、悲观读锁、乐观读。
其中写锁和悲观读锁跟 ReadWriteLock 中的写锁和读锁的概念类似。StampedLock 在使用的时候不一样,加锁的时候会返回一个参数,解锁的时候需要传入这个参数,示例如下:
public class StampedLockTest { private final StampedLock lock = new StampedLock(); private int value; private void addValue(){ long stamp = lock.writeLock(); try { value += 1; } finally { lock.unlockWrite(stamp); } } }
StampedLock 最主要的特点是支持“乐观读”,即当进行读操作的时候,并不是所有的写操作都被阻塞,允许一个线程获取写锁。乐观读的使用示例如下:
public class StampedLockTest { private final StampedLock lock = new StampedLock(); private int value; private void getValue(){ //乐观读,读入变量 long stamp = lock.tryOptimisticRead(); int a = value; //如果验证失败 if (!lock.validate(stamp)){ //升级为悲观读锁,继续读入变量 stamp = lock.readLock(); try { a = value; } finally { lock.unlockRead(stamp); } } } }
需要注意的是,这里使用 validate() 方法进行验证,如果乐观读失败,则升级为悲观读锁,继续获取变量。
2. StampedLock 的注意事项
StampedLock 不支持重入,即不可反复获取同一把锁。
在使用 StampedLock 的时候,不要调用中断操作。如果需要支持中断,可以调用 readLockInterruptibly 和 writeLockInterruptibly 方法。