Java多线程核心技术4 - ReentrantLock 和 ReentrantReadWriteLock 的使用
来自阅读Java多线程编程核心技术的读书笔记,按照自己思路写了一些整理
越读越感觉这本书是一本“口水书”,有堆砌代码案例的嫌疑,看上去感觉更像是一本“博文集”。不是很建议作为自学并发的书籍,但是可以作为初步了解Java并发的窗口,初步熟悉Java中遇到的一些基本方法
历史内容:
Java多线程核心技术1 - 多线程技能
Java多线程核心技术2 - 对象和变量的并发访问
Java多线程核心技术3 - 线程间通信
4. ReentrantLock 和 ReentrantReadWriteLock 的使用
- Java多线程中,可以使用
synchronized
关键字实现线程之间的同步和互斥,Java5+中也可以使用ReentrantLock
类实现同样的效果 ReentrantLock
在拓展功能上更加强大,比如具有嗅探锁定、多路分支等功能,使用起来也比synchronized
更加灵活
4.1 ReentrantLock 可重入锁
文档浏览
ReentrantLock
基本表现与非显式锁synchronized
基本一致,但是有更强大的拓展功能- 公平锁/非公平锁
- 当使用
ReentrantLock(true)
方式创建锁对象,则倾向按照先进先出的方式赋予线程锁 - 否则(
ReentrantLock()/(false)
)将不保证赋予锁权限的顺序 - 使用公平锁将极大的降低吞吐量(性能)
- 当使用
- tryLock() / tryLock(long timeOut, TimeUnit unit)
- 不带时间限制的tryLock方法获得锁时,将不会理会公平性限制;只要锁没上,不管有没有人在等,则都会获得
- lock() 后发生的事情:
- 如果锁没被其他线程抢占,则立即返回,并将lock-hold-count置为1
- 如果当前线程已经获得了锁,则将lock-hold-count增1
- 如果锁已经被其他线程抢占,则休眠直到能够获得锁
ReentrantLock 使用案例
文档中推荐的使用方式:
classX{privatefinal ReentrantLock lock=newReentrantLock();// ...publicvoidm(){ lock.lock();// block until condition holdstry{// ... method body}finally{ lock.unlock()}}}
第一个使用例子:
publicclassLockTest{staticclassMyServiceimplementsRunnable{privatestatic Lock lock=newReentrantLock();// static 所有实例共用一个@Overridepublicvoidrun(){ lock.lock();for(int i=0; i<5; i++){ System.out.println("ThreadName="+ Thread.currentThread().getName()+(" "+(i+1)));} lock.unlock();}}publicstaticvoidmain(String[] args){ Thread t1=newThread(newMyService()); Thread t2=newThread(newMyService()); Thread t3=newThread(newMyService()); Thread t4=newThread(newMyService()); Thread t5=newThread(newMyService()); t1.start(); t2.start(); t3.start(); t4.start(); t5.start();}}
运行结果:
ThreadName=Thread-01 ThreadName=Thread-02 ThreadName=Thread-03 ThreadName=Thread-04 ThreadName=Thread-05 ThreadName=Thread-11 ThreadName=Thread-12 ThreadName=Thread-13 ThreadName=Thread-14 ThreadName=Thread-15 ThreadName=Thread-21 ThreadName=Thread-22 ThreadName=Thread-23 ThreadName=Thread-24 ThreadName=Thread-25 ThreadName=Thread-31 ThreadName=Thread-32 ThreadName=Thread-33 ThreadName=Thread-34 ThreadName=Thread-35 ThreadName=Thread-41 ThreadName=Thread-42 ThreadName=Thread-43 ThreadName=Thread-44 ThreadName=Thread-45
Condition 实现等待/通知
调用
Thread.sleep()
时,并不会释放当前线程占用的锁调用
objLock.wait()
和lock.await()
时,会释放当前线程占用的锁;当等待条件发生后,线程再去争抢锁。但是当争抢到锁的时候,不一定还能满足等待条件! 所以条件判断时,要使用while (conditionIsTrue)
而非if
关键字synchronized
与wait/notify
组合可以实现等待通知模型,但是有不方便之处:不方便实现多路通知功能,即可以将synchronized
中的等待通知看做等在同一个条件变量上
使用ReentrantLock+Condition
可以实现多条件变量的等待/通知,而在synchronzied
中只有单一Condition
方法 | 说明 | 异常 |
---|---|---|
void lock.lock() | 对lock锁上锁 | |
void lock.unlock() | 对lock锁解锁 | |
Condition lock.newCondition() | 获取一个新的Condition | |
void cond.await() | 在cond条件变量上等待 | InterruptException |
void cond.signal() | 唤醒等在cond条件变量上的一个线程 | |
void cond.signalAll() | 唤醒等在cond条件变量上的所有线程 |
要注意:和synchronzied-(wait/notify)
一样,对Condition
的操作同样需要先执行lock.lock()
加锁
Condition 实现生产者-消费者
注:下面代码中,尽量不要将lock.lock()
写到try块内,否则在lock中获取锁(自定义锁)抛出异常时,将导致锁被无故释放!
先看代码:
publicclassPR_Lock_Mul2Mul{static Lock lock=newReentrantLock();static Condition condConsumer= lock.newCondition();static Condition condProducer= lock.newCondition();static String value="";staticclassConsumerimplementsRunnable{@Overridepublicvoidrun(){try{while(true){
lock.lock();while("".equals(value)) condConsumer.await();// consume
System.out.println("Consumer "+ Thread.currentThread().getName()+": "+ value);
value="";
condProducer.signal();
lock.unlock();}}catch(InterruptedException e){
e.printStackTrace();}}}staticclassProducerimplementsRunnable{@Overridepublicvoidrun(){try{while(true){
lock.lock();while(!"".equals(value)) condProducer.await();// produce
String val= System.currentTimeMillis()+"_"+ Thread.currentThread().getName();
System.out.println("Producer "+ Thread.currentThread().getName()+": "+ val);
value= val;
condConsumer.signal();
lock.unlock();}}catch(InterruptedException e){
e.printStackTrace();}}}publicstaticvoidmain(String[] args){
Thread c1=newThread(newConsumer());
Thread c2=newThread(newConsumer());
Thread c3=newThread(newConsumer());
Thread p1=newThread(newProducer());
Thread p2=newThread(newProducer());
Thread p3=newThread(newProducer());
c1.start(); c2.start(); c3.start();
p1.start(); p2.start(); p3.start();}}
整体思路比较简单,大概梳理一下:
对于Producer,生产之前先判断一下是否为空,若不为空则到
condProducer
等待对于Consumer,生产之前先判断是否非空,若为空则到
condConsumer
等待还是注意与OS中管程操作的区别:OS霍尔管程在调用
signal
方法后,将立即将执行权转交给另一个进程,同一时间内仅可能有一个进程可能运行,不会有其他进程抢夺;而Java-ReentrantLock执行signal方法后仅是将被唤醒线程设置为活动,被唤醒线程不一定能占有锁while (!"".equals(value)) condProducer.await();
因此await
处的条件判断均需要用while语句解决,因为下次await处继续运行时并不一定还满足条件Hoare管程结构体定义:
Hoare管程解决生产者消费者问题
公平锁和非公平锁
锁
Lock
分为公平锁与非公平锁- 公平锁表示线程获得锁的顺序是按照加锁顺序分配,即按照先来先得的FIFO顺序
- 非公平锁是一种获取锁的抢占机制,是随机获得锁的,不一定先来先得到锁
创建公平锁与非公平锁
方法 解释 异常 ReentrantLock() ctor 创建非公平锁 ReentrantLock(boolean fair) ctor 根据fair参数值创建公平/非公平锁
三个getCount方法
方法 | 解释 | 异常 |
---|---|---|
lock.getHoldCount() | 查询当前线程保持锁定的个数,即调用lock()的次数 很明显,不可能有多个不同线程同时获得同一个锁,所以只可能是当前线程获得锁的个数 | |
lock.getQueueLength() | 查询等待获得此锁的线程个数 仅为估计值,在查询内部数据结构时可能发生线程动态变化 并不能用于线程同步,仅用于调试 | |
getWaitQueueLength(condition) | 查询等待在条件变量condition上,等待获得此锁的线程个数 |
三个has()方法
方法 | 解释 | 异常 |
---|---|---|
lock.hasQueuedThread(thread) | 查询thread是否在等待获取锁 | |
lock.hasQueuedThreads() | 查询是否有线程在等待获取锁 | |
lock.hasWaiters(cond) | 查询条件变量cond上是否有线程正在等待获取锁 |
两个isHeld/Locked方法
方法 | 解释 | 异常 |
---|---|---|
lock.isHeldByCurrentThread() | 返回是否被当前线程锁定 | |
lock.isLocked() | 返回是否被线程获得锁 |
lockInterruptibly / awaitUninterruptibly
方法 | 解释 | 异常 |
---|---|---|
lock.lockInterruptibly() | 与lock方法类似,当线程已经被中断则抛出异常 | InterruptedException |
cond.awaitUninterruptibly() | 使用await()后,线程被中断将抛出异常 但使用awaitUninterruptibly()后,线程被中断后中断位置false,不会抛出异常 |
* 使用Condition实现顺序执行
要求:对三组线程顺序执行
打印格式:
Thread - GroupId - ThreadIdInGroup
思路:
- 对同一个lock设置三个条件变量,每个线程运行时先检查
now
变量是否轮到自己(now == id) - 若还没轮到自己,到本组的条件变量
conds[id]
上等待即可,注意条件判定用while
- 若已经轮到自己,则继续,执行完毕后设置
now = (now+1)%tot
,并signal对应条件变量
- 对同一个lock设置三个条件变量,每个线程运行时先检查
注意点:千万别搞混
cond.await()
与cond.wait()
!wait/notify
在synchronized语句块中使用,没有条件变量的概念(锁对象对应单一条件变量)await/signal
在Lock对象上的条件变量中使用(锁对象lock可以生成多个条件变量)
/**
* 使用ReentrantLock
* 三组线程顺序执行
*/publicclassOrderExec_Reentrant{privatestaticfinal ReentrantLock lock=newReentrantLock();privatestaticfinal Condition[] conds;privatestaticint now=0;privatestaticint tot=3;static{
conds=newCondition[tot];for(int i=0; i< tot; i++){
conds[i]= lock.newCondition();}}staticclassQueueRunningimplementsRunnable{privatefinalint id;privatefinalint innerId;publicQueueRunning(int id,int innerId){this.id= id;this.innerId= innerId;}@Overridepublicvoidrun(){while(true){
lock.lock();try{// 没轮到自己 - 等待while(now!= id) conds[id].await();
System.out.println("Thread "+ id+"-"+ innerId+": Running");
now=(now+1)% tot;// 自己做完 - 唤醒下一个
conds[now].signal();
Thread.sleep(100);// 稍微停一下}catch(InterruptedException e){
e.printStackTrace();}finally{
System.out.println("Thread "+ id+"-"+ innerId+": Unlock");
lock.unlock();}}}}publicstaticvoidgroupTest(int size){
List<Thread> tl0=newArrayList<>();
List<Thread> tl1=newArrayList<>();
List<Thread> tl2=newArrayList<>();for(int i=0; i< size; i++){
tl0.add(newThread(newQueueRunning(0, i)));
tl1.add(newThread(newQueueRunning(1, i)));
tl2.add(newThread(newQueueRunning(2, i)));}for(int i=0; i< size; i++){
tl0.get(i).start();
tl1.get(i).start();
tl2.get(i).start();}}}
执行结果:
groupTest(1); // 每类仅有一个线程 ### STRAT ### Thread 0-0: Running Thread 0-0: Unlock Thread 1-0: Running Thread 1-0: Unlock Thread 2-0: Running Thread 2-0: Unlock Thread 0-0: Running Thread 0-0: Unlock Thread 1-0: Running Thread 1-0: Unlock Thread 2-0: Running Thread 2-0: Unlock ... ... ### END ### groupTest(10); // 每类10个线程 ### START ### Thread 0-0: Running Thread 0-0: Unlock Thread 1-0: Running Thread 1-0: Unlock Thread 2-0: Running Thread 2-0: Unlock Thread 0-1: Running Thread 0-1: Unlock Thread 1-1: Running Thread 1-1: Unlock Thread 2-1: Running Thread 2-1: Unlock ... ... ### END ###
4.2 ReentrantReadWriteLock 读写锁
使用ReentrantLock
固然好,但是只有完全互斥排他的效果,即同一时间只有一个线程能够获得锁。这样做虽然保障了线程的安全性,但是也降低了效率
JDK中提供ReentrantReadWriteLock
类,在不需要操作实例变量(写操作)时,可以使用读写锁中的读锁,以提高效率
读写锁具有以下特点:
- 读锁之间不互斥
- 读锁和写锁互斥
- 写锁和写锁互斥
操作读锁和写锁
// ReentrantReadWriteLock rwl// readtry{
rwl.readLock().lock();// 上读锁try{// ...}finally{
rwl.readLock().unlock();// 解除读锁}}catch(InterruptedException e){
e.printStackTrace();}// writetry{
rwl.writeLock().lock();// 上写锁try{// ...}finally{
rwl.writeLock().unlock();// 解除写锁}}catch(InterruptedException e){
e.printStackTrace();}