java多线程编程之连续打印abc的几种解法详解编程语言

一道编程题如下:

实例化三个线程,一个线程打印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

 

原创文章,作者:Maggie-Hunter,如若转载,请注明出处:https://blog.ytso.com/17246.html

(0)
上一篇 2021年7月19日
下一篇 2021年7月19日

相关推荐

发表回复

登录后才能评论