多线程协作打印ABC之ReentrantLock版本
Posted 我是攻城师
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程协作打印ABC之ReentrantLock版本相关的知识,希望对你有一定的参考价值。
在前面的文章中:
我们介绍了在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
更多关于锁和线程的文章可参考:
以上是关于多线程协作打印ABC之ReentrantLock版本的主要内容,如果未能解决你的问题,请参考以下文章