一道编程题如下:

实例化三个线程,一个线程打印a,一个线程打印b,一个线程打印c,三个线程同时执行,要求打印出10个连着的abc。

题目分析:

通过题意我们可以得出,本题需要我们使用三个线程,三个线程分别会打印6次字符,关键是如何保证顺序一定是abc...呢。所以此题需要同步机制来解决问题!

令打印字符A的线程为ThreadA,打印B的ThreadB,打印C的为ThreadC。问题为三线程间的同步唤醒操作,主要的目的就是使程序按ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程,因此本人整理出了三种方式来解决此问题。

一、通过两个锁(不推荐,可读性和安全性比较差)

package com.demo.test;

/**
* 基于两个lock实现连续打印abcabc....
* @author lixiaoxi
*
*/
public class TwoLockPrinter implements Runnable { // 打印次数
private static final int PRINT_COUNT = 10;
// 前一个线程的打印锁
private final Object fontLock;
// 本线程的打印锁
private final Object thisLock;
// 打印字符
private final char printChar; public TwoLockPrinter(Object fontLock, Object thisLock, char printChar) {
this.fontLock = fontLock;
this.thisLock = thisLock;
this.printChar = printChar;
} @Override
public void run() {
// 连续打印PRINT_COUNT次
for (int i = 0; i < PRINT_COUNT; i++) {
// 获取前一个线程的打印锁
synchronized (fontLock) {
// 获取本线程的打印锁
synchronized (thisLock) {
//打印字符
System.out.print(printChar);
// 通过本线程的打印锁唤醒后面的线程
// notify和notifyall均可,因为同一时刻只有一个线程在等待
thisLock.notify();
}
// 不是最后一次则通过fontLock等待被唤醒
// 必须要加判断,不然虽然能够打印10次,但10次后就会直接死锁
if(i < PRINT_COUNT - 1){
try {
// 通过fontLock等待被唤醒
fontLock.wait(); } catch (InterruptedException e) {
e.printStackTrace();
}
} }
}
} public static void main(String[] args) throws InterruptedException {
// 打印A线程的锁
Object lockA = new Object();
// 打印B线程的锁
Object lockB = new Object();
// 打印C线程的锁
Object lockC = new Object(); // 打印a的线程
Thread threadA = new Thread(new TwoLockPrinter(lockC, lockA, 'A'));
// 打印b的线程
Thread threadB = new Thread(new TwoLockPrinter(lockA, lockB, 'B'));
// 打印c的线程
Thread threadC = new Thread(new TwoLockPrinter(lockB, lockC, 'C')); // 依次开启a b c线程
threadA.start();
Thread.sleep(100); // 确保按顺序A、B、C执行
threadB.start();
Thread.sleep(100);
threadC.start();
Thread.sleep(100);
} }

打印结果:

ABCABCABCABCABCABCABCABCABCABC

分析:

此解法为了为了确定唤醒、等待的顺序,每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是fontLock,就是前一个线程所持有的对象锁,还有一个就是自身对象锁thisLock。主要的思想就是,为了控制执行的顺序,必须要先持有fontLock锁,也就是前一个线程要释放掉前一个线程自身的对象锁,当前线程再去申请自身对象锁,两者兼备时打印,之后首先调用thisLock.notify()释放自身对象锁,唤醒下一个等待线程,再调用fontLock.wait()释放prev对象锁,暂停当前线程,等待再次被唤醒后进入循环。运行上述代码,可以发现三个线程循环打印ABC,共10次。程序运行的主要过程就是A线程最先运行,持有C,A对象锁,后释放A锁,唤醒B。线程B等待A锁,再申请B锁,后打印B,再释放B锁,唤醒C,线程C等待B锁,再申请C锁,后打印C,再释放C锁,唤醒A。看起来似乎没什么问题,但如果你仔细想一下,就会发现有问题,就是初始条件,三个线程按照A,B,C的顺序来启动,按照前面的思考,A唤醒B,B唤醒C,C再唤醒A。但是这种假设依赖于JVM中线程调度、执行的顺序,所以需要手动控制他们三个的启动顺序,即Thread.Sleep(100)。

二、通过一个ReentrantLock和三个conditon实现(推荐,安全性,性能和可读性较高)

package com.demo.test;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock; /**
* 基于一个ReentrantLock和三个conditon实现连续打印abcabc...
* @author lixiaoxi
*
*/
public class RcSyncPrinter implements Runnable{ // 打印次数
private static final int PRINT_COUNT = 10;
// 打印锁
private final ReentrantLock reentrantLock;
// 本线程打印所需的condition
private final Condition thisCondtion;
// 下一个线程打印所需要的condition
private final Condition nextCondtion;
// 打印字符
private final char printChar; public RcSyncPrinter(ReentrantLock reentrantLock, Condition thisCondtion, Condition nextCondition,
char printChar) {
this.reentrantLock = reentrantLock;
this.nextCondtion = nextCondition;
this.thisCondtion = thisCondtion;
this.printChar = printChar;
} @Override
public void run() {
// 获取打印锁 进入临界区
reentrantLock.lock();
try {
// 连续打印PRINT_COUNT次
for (int i = 0; i < PRINT_COUNT; i++) {
//打印字符
System.out.print(printChar);
// 使用nextCondition唤醒下一个线程
// 因为只有一个线程在等待,所以signal或者signalAll都可以
nextCondtion.signal();
// 不是最后一次则通过thisCondtion等待被唤醒
// 必须要加判断,不然虽然能够打印10次,但10次后就会直接死锁
if (i < PRINT_COUNT - 1) {
try {
// 本线程让出锁并等待唤醒
thisCondtion.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
} }
} finally {
// 释放打印锁
reentrantLock.unlock();
}
} public static void main(String[] args) throws InterruptedException {
// 写锁
ReentrantLock lock = new ReentrantLock();
// 打印a线程的condition
Condition conditionA = lock.newCondition();
// 打印b线程的condition
Condition conditionB = lock.newCondition();
// 打印c线程的condition
Condition conditionC = lock.newCondition();
// 实例化A线程
Thread printerA = new Thread(new RcSyncPrinter(lock, conditionA, conditionB, 'A'));
// 实例化B线程
Thread printerB = new Thread(new RcSyncPrinter(lock, conditionB, conditionC, 'B'));
// 实例化C线程
Thread printerC = new Thread(new RcSyncPrinter(lock, conditionC, conditionA, 'C'));
// 依次开始A B C线程
printerA.start();
Thread.sleep(100);
printerB.start();
Thread.sleep(100);
printerC.start();
}
}

打印结果:

ABCABCABCABCABCABCABCABCABCABC

分析:

仔细想想本问题,既然同一时刻只能有一个线程打印字符,那我们为什么不使用一个同步锁ReentrantLock?线程之间的唤醒操作可以通过Condition实现,且Condition可以有多个,每个condition.await阻塞只能通过该condition的signal/signalall来唤醒!这是synchronized关键字所达不到的,那我们就可以给每个打印线程一个自身的condition和下一个线程的condition,每次打印字符后,调用下一个线程的condition.signal来唤醒下一个线程,然后自身再通过自己的condition.await来释放锁并等待唤醒。

三、通过一个锁和一个状态变量来实现(推荐)

package com.demo.test;

/**
* 基于一个锁和一个状态变量实现连续打印abcabc...
* @author lixiaoxi
*
*/
public class StateLockPrinter {
//状态变量
private volatile int state=0; // 打印线程
private class Printer implements Runnable {
//打印次数
private static final int PRINT_COUNT=10;
//打印锁
private final Object printLock;
//打印标志位 和state变量相关
private final int printFlag;
//后继线程的线程的打印标志位,state变量相关
private final int nextPrintFlag;
//该线程的打印字符
private final char printChar;
public Printer(Object printLock, int printFlag,int nextPrintFlag, char printChar) {
super();
this.printLock = printLock;
this.printFlag=printFlag;
this.nextPrintFlag=nextPrintFlag;
this.printChar = printChar;
} @Override
public void run() {
//获取打印锁 进入临界区
synchronized (printLock) {
//连续打印PRINT_COUNT次
for(int i=0;i<PRINT_COUNT;i++){
//循环检验标志位 每次都阻塞然后等待唤醒
while (state!=printFlag) {
try {
printLock.wait();
} catch (InterruptedException e) {
return;
}
}
//打印字符
System.out.print(printChar);
//设置状态变量为下一个线程的标志位
state=nextPrintFlag;
//注意要notifyall,不然会死锁,因为notify只通知一个,
//但是同时等待的是两个,如果唤醒的不是正确那个就会没人唤醒,死锁了
printLock.notifyAll();
}
}
} } public void test() throws InterruptedException{
//锁
Object lock=new Object();
//打印A的线程
Thread threadA=new Thread(new Printer(lock, 0,1, 'A'));
//打印B的线程
Thread threadB=new Thread(new Printer(lock, 1,2, 'B'));
//打印C的线程
Thread threadC=new Thread(new Printer(lock, 2,0, 'C'));
//一次启动A B C线程
threadA.start();
Thread.sleep(1000);
threadB.start();
Thread.sleep(1000);
threadC.start();
} public static void main(String[] args) throws InterruptedException { StateLockPrinter print = new StateLockPrinter();
print.test();
} }

打印结果:

ABCABCABCABCABCABCABCABCABCABC

分析:

状态变量是一个volatile的整型变量,0代表打印a,1代表打印b,2代表打印c,三个线程都循环检验标志位,通过阻塞前和阻塞后两次判断可以确保当前打印的正确顺序,随后线程打印字符,然后设置下一个状态字符,唤醒其它线程,然后重新进入循环。

补充题

三个Java多线程循环打印递增的数字,每个线程打印5个数值,打印周期1-75,同样的解法:

package com.demo.test;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock; /**
* 数字打印,三个线程同时打印数字,第一个线程打印12345,第二个线程打印678910 .........
* @author lixiaoxi
*
*/
public class NumberPrinter { //打印计数器
private final AtomicInteger counter=new AtomicInteger(0); private class Printer implements Runnable{
//总共需要打印TOTAL_PRINT_COUNT次
private static final int TOTAL_PRINT_COUNT = 5;
//每次打印PER_PRINT_COUNT次
private static final int PER_PRINT_COUNT = 5;
//打印锁
private final ReentrantLock reentrantLock;
//前一个线程的condition
private final Condition afterCondition;
//本线程的condition
private final Condition thisCondtion; public Printer(ReentrantLock reentrantLock, Condition thisCondtion,Condition afterCondition) {
super();
this.reentrantLock = reentrantLock;
this.afterCondition = afterCondition;
this.thisCondtion = thisCondtion;
} @Override
public void run() {
//进入临界区
reentrantLock.lock();
try {
//循环打印TOTAL_PRINT_COUNT次
for(int i=0;i<TOTAL_PRINT_COUNT;i++){
//打印操作
for(int j=0;j<PER_PRINT_COUNT;j++){
//以原子方式将当前值加 1。
//incrementAndGet返回的是新值(即加1后的值)
System.out.println(counter.incrementAndGet());
}
//通过afterCondition通知后面线程
afterCondition.signalAll();
if(i < TOTAL_PRINT_COUNT - 1){
try {
//本线程释放锁并等待唤醒
thisCondtion.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} finally {
reentrantLock.unlock();
}
}
} public void test() throws InterruptedException {
//打印锁
ReentrantLock reentrantLock=new ReentrantLock();
//打印A线程的Condition
Condition conditionA=reentrantLock.newCondition();
//打印B线程的Condition
Condition conditionB=reentrantLock.newCondition();
//打印C线程的Condition
Condition conditionC=reentrantLock.newCondition(); //打印线程A
Thread threadA=new Thread(new Printer(reentrantLock,conditionA, conditionB));
//打印线程B
Thread threadB=new Thread(new Printer(reentrantLock, conditionB, conditionC));
//打印线程C
Thread threadC=new Thread(new Printer(reentrantLock, conditionC, conditionA));
// 依次开启a b c线程
threadA.start();
Thread.sleep(100);
threadB.start();
Thread.sleep(100);
threadC.start();
} public static void main(String[] args) throws InterruptedException {
NumberPrinter print = new NumberPrinter();
print.test();
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

java多线程编程之连续打印abc的几种解法的更多相关文章

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

    一道编程题如下: 实例化三个线程,一个线程打印a,一个打印b,一个打印c,三个线程同时执行,要求打印出6个连着的abc 题目分析: 通过题意我们可以得出,本题需要我们使用三个线程,三个线程分别会打印6 ...

  2. Java多线程编程核心技术

    Java多线程编程核心技术 这本书有利于对Java多线程API的理解,但不容易从中总结规律. JDK文档 1. Thread类 部分源码: public class Thread implements ...

  3. 《Java多线程编程核心技术》知识梳理

    <Java多线程编程核心技术> @author ergwang https://www.cnblogs.com/ergwang/ 文章末尾附pdf和png下载链接 第1章 Java多线程技 ...

  4. Java多线程编程核心技术---学习分享

    继承Thread类实现多线程 public class MyThread extends Thread { @Override public void run() { super.run(); Sys ...

  5. Java多线程编程核心技术---对象及变量的并发访问(二)

    数据类型String的常量池特性 在JVM中具有String常量池缓存的功能. public class Service { public static void print(String str){ ...

  6. Java多线程编程——进阶篇二

    一.线程的交互 a.线程交互的基础知识 线程交互知识点需要从java.lang.Object的类的三个方法来学习:    void notify()           唤醒在此对象监视器上等待的单个 ...

  7. Java多线程编程详解

    转自:http://programming.iteye.com/blog/158568 线程的同步 由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题.Ja ...

  8. 【Todo】【读书笔记】Java多线程编程指南-设计模式篇

    下了这本书<Java多线程编程指南-设计模式篇>, 还有另一本<JAVA多线程设计模式>,据说内容有重复,结合着看.

  9. Java多线程编程总结(学习博客)

    Java多线程编程总结:网址:http://lavasoft.blog.51cto.com/62575/27069/

随机推荐

  1. PHP中$GLOBALS和global的区别

    很多人都认为$GLOBALS['var']和global $var只是写法上不同,其实并不是这样 根据官方的解释是  $GLOBALS['var']是外部全局变量$var的本身, 而global $v ...

  2. ABP学习笔记总汇

    首先立下一个目标,未来一段时间开始学习ABP. 先立一个flag.之后会再次更新目录和文章连接 目录 1.ABP学习笔记(1)-使用mysql

  3. #Java学习之路——基础阶段二(第七篇)

    我的学习阶段是跟着CZBK黑马的双源课程,学习目标以及博客是为了审查自己的学习情况,毕竟看一遍,敲一遍,和自己归纳总结一遍有着很大的区别,在此期间我会参杂Java疯狂讲义(第四版)里面的内容. 前言: ...

  4. 图解 -- Win10 OpenSSH

    一.安装OpenSSH 客户端 .OpenSSH 服务器 设置 -> 管理可选功能 -> 添加功能 -> [OpenSSH 客户端]   [OpenSSH 服务器]  1.设置 2. ...

  5. JavaSSM框架报HTTP Status 500 - Servlet.init() for servlet springMvc threw exception错误

    如下,刚搭建的项目报这样的错,刚学框架的我一脸懵逼...网上很多说是jdk或者springmvc的的jar的版本问题,但是我其他项目都可以啊,所以排除了这个问题. 经过几个小时的排查,发现了我的问题所 ...

  6. Kafka面试题

    1.如何获取topic主题的列表bin/kafka-topics.sh --list --zookeeper localhost:2181 2.生产者和消费者的命令行是什么?生产者在主题上发布消息:b ...

  7. ASP.NET Core Web API 集成测试中使用 Bearer Token

    在 ASP.NET Core Web API 集成测试一文中, 我介绍了ASP.NET Core Web API的集成测试. 在那里我使用了测试专用的Startup类, 里面的配置和开发时有一些区别, ...

  8. 处理SQL Server中的重复行

    如果表中的数据需要基于行中的多个值具有唯一约束,则适合的解决方案将是复合健. 复合主键 使用SQL Server语法创建符合主键非常简单. create table my_parts ( id_par ...

  9. SLAM+语音机器人DIY系列:(八)高阶拓展——1.miiboo机器人安卓手机APP开发

    android要与ROS通讯,一种是基于rosbridge,另一种是基于rosjava库. 相关参考例子工程 rosbridge例子: https://github.com/hibernate2011 ...

  10. vue中用mock制造模拟接口(本文主要解决坑),一定要看完哦

    最近新入职一家公司,后端造接口速度很慢,想来想去还是搞一套模拟接口,来满足开发需求,有人会问,我造一个死数据不就可以了吗?或者说,后端数据结构都没出来,字段我怎么定? 问这个问题的人不奇怪,我之前也有 ...