前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >多线程协作打印ABC之ReentrantLock版本

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

作者头像
我是攻城师
发布2018-12-26 17:14:36
1.1K0
发布2018-12-26 17:14:36
举报
文章被收录于专栏:我是攻城师我是攻城师

在前面的文章中:

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

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

代码语言:javascript
复制
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工具,来实现线程交替打印字符串的功能,源码如下:

代码语言:javascript
复制
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方法还和之前的一样:

代码语言:javascript
复制
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轮,结果如下:

代码语言:javascript
复制
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内存模型的语义

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-12-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 我是攻城师 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档