多线程协作打印ABC之ReentrantLock版本

Posted 我是攻城师

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程协作打印ABC之ReentrantLock版本相关的知识,希望对你有一定的参考价值。


在前面的文章中:


我们介绍了在Java里面使用synchronized + wait/notifyAll实现的多线程轮流打印特定的字符串,输出的结果如下:

 
   
   
 
  1. A线程打印: A

  2. B线程打印: B

  3. C线程打印: C


  4. A线程打印: A

  5. B线程打印: B

  6. C线程打印: C


  7. A线程打印: A

  8. B线程打印: B

  9. C线程打印: C

虽然,使用synchronized内置锁来控制线程协作很容易,但synchronized由于是Java语言里面最早的同步锁方案,因此拥有不少的弊端,总的体现如下:

(1)加锁不具有公平性

(2)一旦获取锁,不能被中断

(3)不具有非阻塞功能,也就是说,在加锁前没法判断,当前是否有线程已经占有了锁。在Lock接口里面,是可以判断是不是有线程正在占有锁。

(4)不具有超时退出功能。

(5)基于Object的监视器对象,线程协作的粒度过粗,不能够精准唤醒指定线程。

这也是为什么在JDK5之后引入java并发工具包(java.util.concurrent)的原因,J.U.C本质上是基于Java语言层面实现的一套高级并发工具,大大丰富了Java对于多线程编程的处理能力,其核心是Doug Lea大神封装的AQS的同步工具器,其中的Lock接口实现的功能提供了对Java锁更灵活的支持。

本篇,我们就来看下如何使用J.U.C的Lock工具,来实现线程交替打印字符串的功能,源码如下:

 
   
   
 
  1.     static class PrintABC{


  2.         Lock lock=new ReentrantLock();


  3.         Condition conA=lock.newCondition();

  4.         Condition conB=lock.newCondition();

  5.         Condition conC=lock.newCondition();

  6.         int limit;//最大打印轮数

  7.         public PrintABC(int limit) {

  8.             this.limit = limit;

  9.         }


  10.         volatile  int count=1;

  11.         String id="A";




  12.         public void printA() throws InterruptedException {

  13.                while(count<limit) {

  14.                 lock.lock();

  15.                 try {

  16.                     while (!id.equals("A")) {

  17.                         conA.await();

  18.                     }

  19.                     System.out.println(Thread.currentThread().getName() + "打印: " + id);

  20.                     id = "B";

  21.                     conB.signal();

  22.                 } finally {

  23.                     lock.unlock();

  24.                 }


  25.             }


  26.         }


  27.         public void printB() throws InterruptedException {

  28.             while(count<limit) {

  29.                 lock.lock();

  30.                 try {

  31.                     while (!id.equals("B")) {

  32.                         conB.await();

  33.                     }

  34.                     System.out.println(Thread.currentThread().getName() + "打印: " + id);

  35.                     id = "C";

  36.                     conC.signal();


  37.                 } finally {

  38.                     lock.unlock();

  39.                 }

  40.             }


  41.         }


  42.         public void printC() throws InterruptedException {



  43.             while (count < limit+1) {

  44.                 lock.lock();

  45.                 try {

  46.                     while (!id.equals("C")) {

  47.                         conC.await();

  48.                     }

  49.                     System.out.println(Thread.currentThread().getName() + "打印: " + id + " \n");

  50.                     id = "A";

  51.                     count = count + 1;

  52.                     conA.signal();


  53.                 } finally {

  54.                     lock.unlock();

  55.                 }


  56.             }

  57.         }





  58.    }

main方法还和之前的一样:

 
   
   
 
  1.        PrintABC printABC=new PrintABC(5);



  2.        Thread t1=new Thread(()->{

  3.            try {

  4.                printABC.printA();

  5.            } catch (InterruptedException e) {

  6.                e.printStackTrace();

  7.            }


  8.        });

  9.        t1.setName("A线程");



  10.        Thread t2=new Thread(()->{

  11.                try {

  12.                    printABC.printB();

  13.                } catch (InterruptedException e) {

  14.                    e.printStackTrace();

  15.                }

  16.        });

  17.        t2.setName("B线程");


  18.        Thread t3=new Thread(()->{

  19.                try {

  20.                    printABC.printC();

  21.                } catch (InterruptedException e) {

  22.                    e.printStackTrace();

  23.                }

  24.        });

  25.        t3.setName("C线程");


  26.        t2.start();

  27.        t3.start();

  28.        t1.start();

这里,我们的A,B,C线程分别有序的共打印5轮,结果如下:

 
   
   
 
  1. A线程打印: A

  2. B线程打印: B

  3. C线程打印: C


  4. A线程打印: A

  5. B线程打印: B

  6. C线程打印: C


  7. A线程打印: A

  8. B线程打印: B

  9. C线程打印: C


  10. A线程打印: A

  11. B线程打印: B

  12. C线程打印: C


  13. A线程打印: A

  14. B线程打印: B

  15. C线程打印: C

下面,我们简单分析下代码:

首先在PrintABC类里面,我们使用了Lock接口的子类ReentrantLock,ReentrantLock是平常Java并发开发中最常用的同步类,从名字里面就能够看出来这个锁是重入锁,当然其他的还有用于特定场景下的支持读写分离的读写锁ReadLock,WriteLock,以及支持锁降级和乐观读的StampedLock,这里不再细说,我之前的文章也介绍过。

扯回正题,我们这里使用了最常用的ReentrantLock来代替内置锁synchronized的功能,同时呢,为了实现线程的协作通信,我们又采用了Lock下面的Condition条件信号量,从例子的代码里面我们能发现,这里为了实现细粒度的唤醒通知,我们从同一个Lock接口的实例里面new出来了3个Condition条件量,这里注意一定要是同一个Lock实例才行,不同的Lock实例是没有效果的,这3个条件信号量,分别用来精准的实现对A,B,C线程通知的控制。

接着我们定义了3个方法,分别用来打印字母A,B,C,每个方法的操作都是通过共享变量和信号通知实现的,在main启动的时候,不管线程的启动顺序如何,第一个打印的总是A线程,其他的线程会进入阻塞,然后在A线程打印完毕之后,会精准的唤醒的B线程打印,这一步需要注意,在synchronized实现的版本中这一步是必须notifyAll来完成的,然后等B线程打印完之后,会唤醒C线程,在执行了同样的操作之后,因为C线程是每一轮的结束,所以在这个地方会对轮次进行控制,因为是最后一轮唤醒,所以在这个地方需要多+1来确保正常结束。这样就实现了多线程协作打印字母的功能。

最后,我们来总结一下关于Lock锁使用时候的几个注意事项:

(1)使用Lock锁的时候,锁的释放一定要放在try-finally块里面,这一点与synchronized不同,synchronized是内置锁,由系统确保锁的释放,不管是否发生异常,但Lock接口需要我们 手动控制。

(2)针对条件量的阻塞,切记一定要放在while循环中,来避免如果发生操作系统虚假唤醒锁的时候,导致发生异常情况。

(3)Lock锁在阻塞获取锁的时候,线程的状态是WATTING,而synchronized锁在阻塞获取锁的时候,线程状态是BLOCKED。它们两者的区别在于前者需要等待其他线程通知自己该去获取锁了,后者是等待其他线程释放锁自己就去抢占。一个是被动,一个是主动。

(4)Lock锁在加锁和释放锁之间的代码是具有happends-before关系的,也就是说和synchronized一样:具有原子性,可见性和有序性的特点。

全部代码,可在我的github上找到:https://github.com/qindongliang/Java-Note

更多关于锁和线程的文章可参考:







以上是关于多线程协作打印ABC之ReentrantLock版本的主要内容,如果未能解决你的问题,请参考以下文章

多线程顺序打印ABC

LeetCode解题之十七:循环打印ABC

JDK并发工具之多线程团队协作:同步控制

java多线程编程之连续打印abc的几种解法

python 多线程实现循环打印 abc

不讲武德(手动狗头):面试官上来就甩给我几道多线程代码题叫我手撕,我心里拔凉拔凉的~~~