Java多线程核心技术ReentrantLock和ReentrantReadWriteLock 的使用

2022-07-19 11:48:36

来自阅读Java多线程编程核心技术的读书笔记,按照自己思路写了一些整理
越读越感觉这本书是一本“口水书”,有堆砌代码案例的嫌疑,看上去感觉更像是一本“博文集”。不是很建议作为自学并发的书籍,但是可以作为初步了解Java并发的窗口,初步熟悉Java中遇到的一些基本方法
历史内容:
Java多线程核心技术1 - 多线程技能
Java多线程核心技术2 - 对象和变量的并发访问
Java多线程核心技术3 - 线程间通信

4. ReentrantLock 和 ReentrantReadWriteLock 的使用

  • Java多线程中,可以使用synchronized关键字实现线程之间的同步和互斥,Java5+中也可以使用ReentrantLock类实现同样的效果
  • ReentrantLock在拓展功能上更加强大,比如具有嗅探锁定、多路分支等功能,使用起来也比synchronized更加灵活

4.1 ReentrantLock 可重入锁

文档浏览

来自java.util.concurrent.locks.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

关键字synchronizedwait/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管程结构体定义:

      image-20210124122103823

    • Hoare管程解决生产者消费者问题

      image-20210124122011179

公平锁和非公平锁

  • 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对应条件变量
  • 注意点:千万别搞混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();}
  • 作者:JinyuChu
  • 原文链接:https://blog.csdn.net/weixin_42593937/article/details/113091982
    更新时间:2022-07-19 11:48:36