一道编程题如下:
实例化三个线程,一个线程打印a,一个打印b,一个打印c,三个线程同时执行,要求打印出6个连着的abc
题目分析:
通过题意我们可以得出,本题需要我们使用三个线程,三个线程分别会打印6次字符,关键是如何保证顺序一定是abc...呢。所以此题需要同步机制来解决问题!
令打印字符A的线程为ThreadA,打印B的ThreadB,打印C的为ThreadC。问题为三线程间的同步唤醒操作,主要的目的就是使程序按ThreadA->ThreadB->ThreadC-
>ThreadA循环执行三个线程,因此本人整理出了三种方式来解决此问题。
一.通过两个锁(不推荐,可读性和安全性比较差)
/*** 基于两个lock实现连续打印abcabc....
*@authorfhr
*@since2017/09/04*/
public classTwoLockPrinter {
@Testpublic void test() throwsInterruptedException {//打印A线程的锁
Object lockA = newObject();//打印B线程的锁
Object lockB = newObject();//打印C线程的锁
Object lockC = newObject();
ThreadGroup group= new ThreadGroup("xx");//打印a的线程
Thread threadA = new Thread(group, new Printer(lockC, lockA, 'A'));//打印b的线程
Thread threadB = new Thread(group, new Printer(lockA, lockB, 'B'));//打印c的线程
Thread threadC = new Thread(group, new Printer(lockB, lockC, 'C'));//依次开启a b c线程
threadA.start();
Thread.sleep(100);
threadB.start();
Thread.sleep(100);
threadC.start();//主线程循环让出cpu使用权
while (group.activeCount() > 0) {
Thread.yield();
}
}//打印线程
private class Printer implementsRunnable {//打印次数
private static final int PRINT_COUNT = 6;//前一个线程的打印锁
private finalObject fontLock;//本线程的打印锁
private finalObject thisLock;//打印字符
private final charprintChar;public Printer(Object fontLock, Object thisLock, charprintChar) {super();this.fontLock =fontLock;this.thisLock =thisLock;this.printChar =printChar;
}
@Overridepublic voidrun() {//连续打印PRINT_COUNT次
for (int i = 0; i < PRINT_COUNT; i++) {//获取前一个线程的打印锁
synchronized(fontLock) {//获取本线程的打印锁
synchronized(thisLock) {//打印字符
System.out.print(printChar);//通过本线程的打印锁唤醒后面的线程//notify和notifyall均可,因为同一时刻只有一个线程在等待
thisLock.notify();//不是最后一次则通过fontLock等待被唤醒//必须要加判断,不然能够打印6次 但6次后就会直接死锁
if (i < PRINT_COUNT - 1) {try{//通过fontLock等待被唤醒
fontLock.wait();
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
}
此解法为了为了确定唤醒、等待的顺序,每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是fontLock,就是前一个线程所持有的对象锁,还有一个就是自身
对象锁thisLock。主要的思想就是,为了控制执行的顺序,必须要先持有fontLock锁,也就是前一个线程要释放掉前一个线程自身的对象锁,当前线程再去申请自身对象锁,两
者兼备时打印,之后首先调用thisLock.notify()释放自身对象锁,唤醒下一个等待线程,再调用fontLock.wait()释放prev对象锁,暂停当前线程,等待再次被唤醒后进入循环。运
行上述代码,可以发现三个线程循环打印ABC,共6次。程序运行的主要过程就是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实现(推荐,安全性,性能和可读性较高)
/*** 基于一个ReentrantLock和三个conditon实现连续打印abcabc...
*@authorfhr
*@since2017/09/04*/
public classRcSyncPrinter {
@Testpublic void test() throwsInterruptedException {
ThreadGroup group= new ThreadGroup("xx");//写锁
ReentrantLock lock = newReentrantLock();//打印a线程的condition
Condition conditionA =lock.newCondition();//打印b线程的condition
Condition conditionB =lock.newCondition();//打印c线程的condition
Condition conditionC =lock.newCondition();//实例化A线程
Thread printerA = new Thread(group, new Printer(lock, conditionA, conditionB, 'A'));//实例化B线程
Thread printerB = new Thread(group, new Printer(lock, conditionB, conditionC, 'B'));//实例化C线程
Thread printerC = new Thread(group, new Printer(lock, conditionC, conditionA, 'C'));//依次开始A B C线程
printerA.start();
Thread.sleep(100);
printerB.start();
Thread.sleep(100);
printerC.start();//主线程循环让出CPU使用权
while (group.activeCount() > 0) {
Thread.yield();
}
}//打印线程
private class Printer implementsRunnable {//打印次数
private static final int PRINT_COUNT = 6;//打印锁
private finalReentrantLock reentrantLock;//本线程打印所需的condition
private finalCondition thisCondtion;//下一个线程打印所需要的condition
private finalCondition nextCondtion;//打印字符
private final charprintChar;public Printer(ReentrantLock reentrantLock, Condition thisCondtion, Condition nextCondition, charprintChar) {this.reentrantLock =reentrantLock;this.nextCondtion =nextCondition;this.thisCondtion =thisCondtion;this.printChar =printChar;
}
@Overridepublic voidrun() {//获取打印锁 进入临界区
reentrantLock.lock();try{//连续打印PRINT_COUNT次
for (int i = 0; i < PRINT_COUNT; i++) {
System.out.print(printChar);//使用nextCondition唤醒下一个线程//因为只有一个线程在等待,所以signal或者signalAll都可以
nextCondtion.signal();//不是最后一次则通过thisCondtion等待被唤醒//必须要加判断,不然能够打印6次 但6次后就会直接死锁
if (i < PRINT_COUNT - 1) {try{//本线程让出锁并等待唤醒
thisCondtion.await();
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}finally{//释放打印锁
reentrantLock.unlock();
}
}
}
}
仔细想想本问题,既然同一时刻只能有一个线程打印字符,那我们为什么不使用一个同步锁ReentrantLock?线程之间的唤醒操作可以通过Condition实现,且Condition可以有
多个,每个condition.await阻塞只能通过该condition的signal/signalall来唤醒!这是synchronized关键字所达不到的,那我们就可以给每个打印线程一个自身的condition和下一
个线程的condition,每次打印字符后,调用下一个线程的condition.signal来唤醒下一个线程,然后自身再通过自己的condition.await来释放锁并等待唤醒。
三.通过一个锁和一个状态变量来实现(推荐)
/*** 基于一个锁和一个状态变量实现连续打印abcabc...
*@authorfhr
*@since2017/09/04*/
public classStateLockPrinter {//状态变量
private volatile int state=0;
@Testpublic void test() throwsInterruptedException {//锁
Object lock=newObject();
ThreadGroup group=new ThreadGroup("xx");//打印A的线程
Thread threadA=new Thread(group,new Printer(lock, 0,1, 'A'));//打印B的线程
Thread threadB=new Thread(group,new Printer(lock, 1,2, 'B'));//打印C的线程
Thread threadC=new Thread(group,new Printer(lock, 2,0, 'C'));//一次启动A B C线程
threadA.start();
Thread.sleep(1000);
threadB.start();
Thread.sleep(1000);
threadC.start();//循环检查线程组合的活的线程数量
while (group.activeCount()>0) {//让出CPU使用权
Thread.yield();
}
}//打印线程
private class Printer implementsRunnable{//打印次数
private static final int PRINT_COUNT=6;//打印锁
private finalObject printLock;//打印标志位 和state变量相关
private final intprintFlag;//后继线程的线程的打印标志位,state变量相关
private final intnextPrintFlag;//该线程的打印字符
private final charprintChar;public Printer(Object printLock, int printFlag,int nextPrintFlag, charprintChar) {super();this.printLock =printLock;this.printFlag=printFlag;this.nextPrintFlag=nextPrintFlag;this.printChar =printChar;
}
@Overridepublic voidrun() {//获取打印锁 进入临界区
synchronized(printLock) {//连续打印PRINT_COUNT次
for(int i=0;i
while (state!=printFlag) {try{
printLock.wait();
}catch(InterruptedException e) {return;
}
}//打印字符
System.out.print(printChar);//设置状态变量为下一个线程的标志位
state=nextPrintFlag;//注意要notifyall,不然会死锁,因为notify只通知一个,//但是同时等待的是两个,如果唤醒的不是正确那个就会没人唤醒,死锁了
printLock.notifyAll();
}
}
}
}
}
状态变量是一个volatile的整型变量,0代表打印a,1代表打印b,2代表打印c,三个线程都循环检验标志位,通过阻塞前和阻塞后两次判断可以确保当前打印的正确顺序,随后线程
打印字符,然后设置下一个状态字符,唤醒其它线程,然后重新进入循环。
补充题
三个Java多线程循环打印递增的数字,每个线程打印5个数值,打印周期1-75,同样的解法:
/*** 数字打印,三个线程同时打印数字,
* 第一个线程打印12345,第二个线程打印678910 .........
*@authorfhr
*@since2017/09/04*/
public classNumberPrinter {//打印计数器
private final AtomicInteger counter=new AtomicInteger(0);
@Testpublic void test() throwsInterruptedException {//打印锁
ReentrantLock reentrantLock=newReentrantLock();//打印A线程的Condition
Condition conditionA=reentrantLock.newCondition();//打印B线程的Condition
Condition conditionB=reentrantLock.newCondition();//打印C线程的Condition
Condition conditionC=reentrantLock.newCondition();
ThreadGroup group=new ThreadGroup("xx");//打印线程A
Thread threadA=new Thread(group,newPrinter(reentrantLock,conditionA, conditionB));//打印线程B
Thread threadB=new Thread(group,newPrinter(reentrantLock, conditionB, conditionC));//打印线程C
Thread threadC=new Thread(group,newPrinter(reentrantLock, conditionC, conditionA));//依次开启a b c线程
threadA.start();
Thread.sleep(100);
threadB.start();
Thread.sleep(100);
threadC.start();while (group.activeCount()>0) {
Thread.yield();
}
}private class Printer implementsRunnable{//总共需要打印TOTAL_PRINT_COUNT次
private static final int TOTAL_PRINT_COUNT=5;//每次打印PER_PRINT_COUNT次
private static final int PER_PRINT_COUNT=5;//打印锁
private finalReentrantLock reentrantLock;//前一个线程的condition
private finalCondition afterCondition;//本线程的condition
private finalCondition thisCondtion;publicPrinter(ReentrantLock reentrantLock, Condition thisCondtion,Condition afterCondition) {super();this.reentrantLock =reentrantLock;this.afterCondition =afterCondition;this.thisCondtion =thisCondtion;
}
@Overridepublic voidrun() {//进入临界区
reentrantLock.lock();try{//循环打印TOTAL_PRINT_COUNT次
for(int i=0;i
for(int j=0;j
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();
}
}
}
}