多线程协作打印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
更多关于锁和线程的文章可参考:
理解AbstractQueuedSynchronizer提供的独占锁和共享锁语义
- GoldenGate安装简记(r10笔记第78天)
- 【Go 语言社区】各种变量的声明
- 【Go 语言社区】Golang 高效字符串拼接
- 实战 | Elasticsearch实现类Google高级检索
- Golang中time包用法--转
- 干货 | Elasticsearch 集群健康值红色终极解决方案
- Go语言interface的value.(type)使用小技巧-转
- 干货 | Elasticsearch5.X Mapping万能模板
- MySQL 5.7安装部署总结(r10笔记第77天)
- Go语言中Socket通信TCP服务端
- MySQL和Oracle的添加字段的处理差别 (r10笔记第73天)
- MySQL修改数据类型的问题总结(r10笔记第74天)
- 深究|Elasticsearch单字段支持的最大字符数?
- Go语言中Socket通信之Tcp客户端
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 高性能无锁并发框架 Disruptor,太强了!
- Spring Boot 太狠了,一口气发布了 3 个版本!
- 贷款违约预测-Task2 数据分析
- Redis 最牛实践:业务层面和运维层面优化!
- 一个 randomkey 命令导致的 Redis 事故。。
- 分布式锁(数据库、Redis、ZK)拍了拍你
- 贷款违约预测-Task3 特征工程
- 用SQL代替DSL查询ElasticSearch怎样?
- 面试造飞机:面对Redis持久化连环Call,你还顶得住吗?
- 体验spring-boot-devtools热部署,流畅且不失强大,Jrebel呢?
- 贷款诈骗 x 摸版0day + 实战预警脚本
- 你不知道的 Linux 使用技巧
- 一文详解 Websocket 的前世今生
- 实例 | 教你用Python写一个电信客户流失预测模型
- OpenCV快速傅里叶变换(FFT)用于图像和视频流的模糊检测