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

在前面的文章中:

多个线程如何轮流打印ABC特定的次数?

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

A线程打印: A
B线程打印: B
C线程打印: C

A线程打印: A
B线程打印: B
C线程打印: C

A线程打印: A
B线程打印: B
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工具,来实现线程交替打印字符串的功能,源码如下:

static class PrintABC{

         Lock lock=new ReentrantLock();

         Condition conA=lock.newCondition();
         Condition conB=lock.newCondition();
         Condition conC=lock.newCondition();
         int limit;//最大打印轮数
         public PrintABC(int limit) {
             this.limit = limit;
         }

         volatile  int count=1;
         String id="A";



         public void printA() throws InterruptedException {
                while(count<limit) {
                 lock.lock();
                 try {
                     while (!id.equals("A")) {
                         conA.await();
                     }
                     System.out.println(Thread.currentThread().getName() + "打印: " + id);
                     id = "B";
                     conB.signal();
                 } finally {
                     lock.unlock();
                 }

             }

         }

         public void printB() throws InterruptedException {
             while(count<limit) {
                 lock.lock();
                 try {
                     while (!id.equals("B")) {
                         conB.await();
                     }
                     System.out.println(Thread.currentThread().getName() + "打印: " + id);
                     id = "C";
                     conC.signal();

                 } finally {
                     lock.unlock();
                 }
             }

         }

         public void printC() throws InterruptedException {


             while (count < limit+1) {
                 lock.lock();
                 try {
                     while (!id.equals("C")) {
                         conC.await();
                     }
                     System.out.println(Thread.currentThread().getName() + "打印: " + id + " \n");
                     id = "A";
                     count = count + 1;
                     conA.signal();

                 } finally {
                     lock.unlock();
                 }

             }
         }




    }

main方法还和之前的一样:

PrintABC printABC=new PrintABC(5);


        Thread t1=new Thread(()->{
            try {
                printABC.printA();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

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


        Thread t2=new Thread(()->{
                try {
                    printABC.printB();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        });
        t2.setName("B线程");

        Thread t3=new Thread(()->{
                try {
                    printABC.printC();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        });
        t3.setName("C线程");

        t2.start();
        t3.start();
        t1.start();

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

A线程打印: A
B线程打印: B
C线程打印: C 

A线程打印: A
B线程打印: B
C线程打印: C 

A线程打印: A
B线程打印: B
C线程打印: C 

A线程打印: A
B线程打印: B
C线程打印: C 

A线程打印: A
B线程打印: B
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

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

理解Java中锁的状态与优化

理解AbstractQueuedSynchronizer提供的独占锁和共享锁语义

关于自旋锁的公平和非公平模式

Java并发之高级自旋锁CLH锁和MCS锁

深入理解Java内存模型的语义

原文发布于微信公众号 - 我是攻城师(woshigcs)

原文发表时间:2018-12-11

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区