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

一道编程题如下:

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

题目分析:

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

令打印字符A的线程为ThreadA,打印B的ThreadB,打印C的为ThreadC。问题为三线程间的同步唤醒操作,主要的目的就是使程序按ThreadA->ThreadB->ThreadC-

>ThreadA循环执行三个线程,因此本人整理出了三种方式来解决此问题。

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

/** * 基于两个lock实现连续打印abcabc.... * @author fhr * @since 2017/09/04 */public class TwoLockPrinter {    @Test    public void test() throws InterruptedException {        // 打印A线程的锁        Object lockA = new Object();        // 打印B线程的锁        Object lockB = new Object();        // 打印C线程的锁        Object lockC = new Object();        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 implements Runnable {        // 打印次数        private static final int PRINT_COUNT = 6;        // 前一个线程的打印锁        private final Object fontLock;        // 本线程的打印锁        private final Object thisLock;        // 打印字符        private final char printChar;        public Printer(Object fontLock, Object thisLock, char printChar) {            super();            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等待被唤醒                        // 必须要加判断,不然能够打印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... * @author fhr * @since 2017/09/04 */public class RcSyncPrinter {    @Test    public void test() throws InterruptedException {        ThreadGroup group = new ThreadGroup("xx");        // 写锁        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(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 implements Runnable {        // 打印次数        private static final int PRINT_COUNT = 6;        // 打印锁        private final ReentrantLock reentrantLock;        // 本线程打印所需的condition        private final Condition thisCondtion;        // 下一个线程打印所需要的condition        private final Condition nextCondtion;        // 打印字符        private final char printChar;        public Printer(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等待被唤醒                    // 必须要加判断,不然能够打印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... * @author fhr * @since 2017/09/04 */public class StateLockPrinter {    //状态变量    private volatile int state=0;    @Test    public void test() throws InterruptedException {        //锁        Object lock=new Object();        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 implements Runnable{        //打印次数        private static final int PRINT_COUNT=6;        //打印锁        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();                    }            }        }    }}

状态变量是一个volatile的整型变量,0代表打印a,1代表打印b,2代表打印c,三个线程都循环检验标志位,通过阻塞前和阻塞后两次判断可以确保当前打印的正确顺序,随后线程

打印字符,然后设置下一个状态字符,唤醒其它线程,然后重新进入循环。

补充题

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

/** * 数字打印,三个线程同时打印数字, * 第一个线程打印12345,第二个线程打印678910 ......... * @author fhr * @since 2017/09/04 */public class NumberPrinter {    //打印计数器    private final AtomicInteger counter=new AtomicInteger(0);        @Test    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();        ThreadGroup group=new ThreadGroup("xx");        //打印线程A        Thread threadA=new Thread(group,new Printer(reentrantLock,conditionA, conditionB));        //打印线程B        Thread threadB=new Thread(group,new Printer(reentrantLock, conditionB, conditionC));        //打印线程C        Thread threadC=new Thread(group,new Printer(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 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++){                        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();            }        }    }}

君子无故,玉不去身。

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

相关文章:

你感兴趣的文章:

标签云: