三个线程交替打印
示例线程1打印A,线程2打印B,线程3打印C,要求交替打印,并且可以循环打印。 输出结果类似ABCABCABC 这道题的难度是五颗星,在面试中也会经常遇到,如果是第一次见到这道题,很难在短时间内想出合理的解决方案。 如果只要求交替打印一次的话,实现比较简单,可以用Thread.join()方法,一个线程等待另一个线程执行完成。 现在要求循环打印,就涉及线程间通信,必须要用到锁,一把锁肯定不够。例如线程1释放锁之后,线程2和线程3都可能获取到锁,是随机的,现在要求必须是线程2获取到锁,所以需要三把锁。 执行过程: 线程1获取A锁,打印A,释放B锁; 线程2获取B锁,打印B,释放C锁; 线程3获取C锁,打印C,释放A锁; 循环执行; 可以用Synchronized或者ReentrantLock实现,不过它们不能控制线程启动后的执行顺序。因为三个线程启动后,都等待CPU调度执行,而CPU调度的顺序又是随机的,所以不能保证线程1先执行。 有个可以控制线程启动后执行顺序,又简单的实现方式,就是用Semaphore(信号量),它可以控制共享资源的访问个数。 使用方式:
- 初始化的时候,指定共享资源的个数
// 初始化一个资源Semaphore semaphore = new Semaphore(1);- 获取资源,获取资源后,semaphore资源个数减1,变成0,其他线程再获取资源的时候就会阻塞等待
semaphore.acquire();
- 释放资源,semaphore资源个数加1,其他阻塞的线程就可以获取到资源了
semaphore.release(); 代码实现:
/** * @author yideng * @apiNote 三个线程循环打印 */public class CirclePrint {
static class ThreadDemo extends Thread {
private Semaphore current; private Semaphore next; private String name;
/** * 构造方法 * @param current 要获取的当前锁 * @param next 要释放的下一把锁 * @param name 打印内容 */ public ThreadDemo(Semaphore current, Semaphore next, String name) { this.current = current; this.next = next; this.name = name; }
@Override public void run() { for (int i = 0; i < 5; i++) { try { // 获取当前锁,然后打印 current.acquire(); System.out.print(name); } catch (InterruptedException e) { e.printStackTrace(); } // 释放下一把锁 next.release(); } } }
public static void main(String[] args) { // 初始化三把锁,只有A锁是可用的 Semaphore A = new Semaphore(1); Semaphore B = new Semaphore(0); Semaphore C = new Semaphore(0);
// 创建并启动三个线程,线程1获取A锁,释放B锁 new ThreadDemo(A, B, "A").start(); // 线程2获取B锁,释放C锁 new ThreadDemo(B, C, "B").start(); // 线程3获取C锁,释放A锁 new ThreadDemo(C, A, "C").start(); }}输出结果: ABCABCABCABCABC
加餐 五种方式实现三个线程交替打印
正文: 这是一道面试中常考的并发编程的代码题,与它相似的问题有:
- 三个线程T1、T2、T3轮流打印ABC,打印n次,如ABCABCABCABC…
- 两个线程交替打印1-100的奇偶数
- N个线程循环打印1-100
- …
其实这类问题本质上都是线程通信问题,思路基本上都是一个线程执行完毕,阻塞该线程,唤醒其他线程,按顺序执行下一个线程。下面先来看最简单的,如何按顺序执行三个线程。
synchronized+wait/notify
基本思路就是线程A、线程B、线程C三个线程同时启动,因为变量num的初始值为0,所以线程B或线程C拿到锁后,进入while()循环,然后执行wait()方法,线程线程阻塞,释放锁。只有线程A拿到锁后,不进入while()循环,执行num++,打印字符A,最后唤醒线程B和线程C。此时num值为1,只有线程B拿到锁后,不被阻塞,执行num++,打印字符B,最后唤醒线程A和线程C,后面以此类推。
class Wait_Notify_ACB {
private int num; private static final Object LOCK = new Object();
private void printABC(int targetNum) { synchronized (LOCK) { while (num % 3 != targetNum) { //想想这里为什么不能用if代替while,想不起来可以看公众号上一篇文章 try { LOCK.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } num++; System.out.print(Thread.currentThread().getName()); LOCK.notifyAll(); } }
public static void main(String[] args) { Wait_Notify_ACB wait_notify_acb = new Wait_Notify_ACB (); new Thread(() -> { wait_notify_acb.printABC(0); }, "A").start(); new Thread(() -> { wait_notify_acb.printABC(1); }, "B").start(); new Thread(() -> { wait_notify_acb.printABC(2); }, "C").start(); }}输入结果:
ABCProcess finished with exit code 0接下来看看第一个问题,三个线程T1、T2、T3轮流打印ABC,打印n次。其实只需要将上述代码加一个循环即可,这里假设n=10。
class Wait_Notify_ACB {
private int num; private static final Object LOCK = new Object();
private void printABC(int targetNum) { for (int i = 0; i < 10; i++) { synchronized (LOCK) { while (num % 3 != targetNum) { //想想这里为什么不能用if代替,想不起来可以看公众号上一篇文章 try { LOCK.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } num++; System.out.print(Thread.currentThread().getName()); LOCK.notifyAll(); } } } public static void main(String[] args) { Wait_Notify_ACB wait_notify_acb = new Wait_Notify_ACB (); new Thread(() -> { wait_notify_acb.printABC(0); }, "A").start(); new Thread(() -> { wait_notify_acb.printABC(1); }, "B").start(); new Thread(() -> { wait_notify_acb.printABC(2); }, "C").start(); }}输出结果:
ABCABCABCABCABCABCABCABCABCABCProcess finished with exit code 0下面看第二个问题,两个线程交替打印1-100的奇偶数,为了减少输入所占篇幅,这里将100 改成了10。基本思路上面类似,线程odd先拿到锁——打印数字——唤醒线程even——阻塞线程odd,以此循环。
class Wait_Notify_Odd_Even{
private Object monitor = new Object(); private volatile int count;
Wait_Notify_Odd_Even(int initCount) { this.count = initCount; }
private void printOddEven() { synchronized (monitor) { while (count < 10) { try { System.out.print( Thread.currentThread().getName() + ":"); System.out.println(++count); monitor.notifyAll(); monitor.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //防止count=10后,while()循环不再执行,有子线程被阻塞未被唤醒,导致主线程不能退出 monitor.notifyAll(); } }
public static void main(String[] args) throws InterruptedException {
Wait_Notify_Odd_Even waitNotifyOddEven = new Wait_Notify_Odd_Even(0); new Thread(waitNotifyOddEven::printOddEven, "odd").start(); Thread.sleep(10); //为了保证线程odd先拿到锁 new Thread(waitNotifyOddEven::printOddEven, "even").start(); }}运行结果:
odd:1even:2odd:3even:4odd:5even:6odd:7even:8odd:9even:10再看第三个问题,N个线程循环打印1-100,其实仔细想想这个和三个线程循环打印ABC并没有什么本质区别,只需要加上判断是否到了打印数字的最大值的语句即可。假设N=3,为了能把输出结果完全显示,打印1-10,代码如下:
class Wait_Notify_100 {
private int num; private static final Object LOCK = new Object(); private int maxnum = 10;
private void printABC(int targetNum) { while (true) { synchronized (LOCK) { while (num % 3 != targetNum) { //想想这里为什么不能用if代替,想不起来可以看公众号上一篇文章 if(num >= maxnum){ break; } try { LOCK.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } if(num >= maxnum){ break; } num++; System.out.println(Thread.currentThread().getName() + ": " + num); LOCK.notifyAll(); } }
}
public static void main(String[] args) { Wait_Notify_100 wait_notify_100 = new Wait_Notify_100 (); new Thread(() -> { wait_notify_100.printABC(0); }, "thread1").start(); new Thread(() -> { wait_notify_100.printABC(1); }, "thread2").start(); new Thread(() -> { wait_notify_100.printABC(2); }, "thread3").start(); }}输出结果:
thread1: 1thread2: 2thread3: 3thread1: 4thread2: 5thread3: 6thread1: 7thread2: 8thread3: 9thread1: 10**面试官:**大家都是用的synchronized+wait/notify,你能不能换个方法解决该问题? **我:**好的,我还会用join()方法 下面介绍的方法只给出第一道题的代码了,否则太长了,相信大家可以举一反三
join()
join()方法:在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。基于这个原理,我们使得三个线程按顺序执行,然后循环多次即可。无论线程1、线程2、线程3哪个先执行,最后执行的顺序都是线程1——>线程2——>线程3。代码如下:
class Join_ABC {
public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10; i++) { Thread t1 = new Thread(new printABC(null),"A"); Thread t2 = new Thread(new printABC(t1),"B"); Thread t3 = new Thread(new printABC(t2),"C"); t0.start(); t1.start(); t2.start(); Thread.sleep(10); //这里是要保证只有t1、t2、t3为一组,进行执行才能保证t1->t2->t3的执行顺序。 }
}
static class printABC implements Runnable{ private Thread beforeThread; public printABC(Thread beforeThread) { this.beforeThread = beforeThread; } @Override public void run() { if(beforeThread!=null) { try { beforeThread.join(); System.out.print(Thread.currentThread().getName()); }catch(Exception e){ e.printStackTrace(); } }else { System.out.print(Thread.currentThread().getName()); }
} }}输出结果:
ABCABCABCABCABCABCABCABCABCABC**面试官:**还会其他方法吗? **我:**还会使用Lock解决该问题。
Lock
该方法很容易理解,不管哪个线程拿到锁,只有符合条件的才能打印。代码如下:
class Lock_ABC {
private int num; // 当前状态值:保证三个线程之间交替打印 private Lock lock = new ReentrantLock();
private void printABC(int targetNum) { for (int i = 0; i < 10; ) { lock.lock(); if (num % 3 == targetNum) { num++; i++; System.out.print(Thread.currentThread().getName()); } lock.unlock(); } }
public static void main(String[] args) { Lock_ABC lockABC = new Lock_ABC();
new Thread(() -> { lockABC.printABC(0); }, "A").start();
new Thread(() -> { lockABC.printABC(1); }, "B").start();
new Thread(() -> { lockABC.printABC(2); }, "C").start(); }}输出结果:
ABCABCABCABCABCABCABCABCABCABC**面试官:**该方法存在什么问题,可以进一步优化吗 **我:**可以使用Lock+Condition实现对线程的精准唤醒,减少对同步锁的无意义竞争,浪费资源。
Lock+Condition
该思路和synchronized+wait/notify方法的很像,synchronized对应lock,await/signal方法对应wait/notify方法。下面的代码为了能精准地唤醒下一个线程,创建了多个Condition对象。
class LockConditionABC {
private int num; private static Lock lock = new ReentrantLock(); private static Condition c1 = lock.newCondition(); private static Condition c2 = lock.newCondition(); private static Condition c3 = lock.newCondition();
private void printABC(int targetNum, Condition currentThread, Condition nextThread) { for (int i = 0; i < 10; ) { lock.lock(); try { while (num % 3 != targetNum) { currentThread.await(); //阻塞当前线程 } num++; i++; System.out.print(Thread.currentThread().getName()); nextThread.signal(); //唤醒下一个线程,而不是唤醒所有线程 } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
public static void main(String[] args) { LockConditionABC print = new LockConditionABC(); new Thread(() -> { print.printABC(0, c1, c2); }, "A").start(); new Thread(() -> { print.printABC(1, c2, c3); }, "B").start(); new Thread(() -> { print.printABC(2, c3, c1); }, "C").start(); }}输出结果:
ABCABCABCABCABCABCABCABCABCABC**面试官:**除了该方法,还有什么方法可以避免唤醒其他无意义的线程避免资源浪费? **我:**可以通过使用信号量来实现。
Semaphore
Semaphore:用来控制同时访问某个特定资源的操作数量,或者同时执行某个制定操作的数量。Semaphore内部维护了一个计数器,其值为可以访问的共享资源的个数。 一个线程要访问共享资源,先使用acquire()方法获得信号量,如果信号量的计数器值大于等于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。如果计数器值为0,线程进入休眠。 当某个线程使用完共享资源后,使用release()释放信号量,并将信号量内部的计数器加1,之前进入休眠的线程将被唤醒并再次试图获得信号量。 代码如下:
class SemaphoreABC {
private static Semaphore s1 = new Semaphore(1); //因为先执行线程A,所以这里设s1的计数器为1 private static Semaphore s2 = new Semaphore(0); private static Semaphore s3 = new Semaphore(0);
private void printABC(Semaphore currentThread, Semaphore nextThread) { for (int i = 0; i < 10; i++) { try { currentThread.acquire(); //阻塞当前线程,即信号量的计数器减1为0 System.out.print(Thread.currentThread().getName()); nextThread.release(); //唤醒下一个线程,即信号量的计数器加1
} catch (InterruptedException e) { e.printStackTrace(); } } }
public static void main(String[] args) throws InterruptedException { SemaphoreABC printer = new SemaphoreABC(); new Thread(() -> { printer.printABC(s1, s2); }, "A").start(); Thread.sleep(10); new Thread(() -> { printer.printABC(s2, s3); }, "B").start(); Thread.sleep(10); new Thread(() -> { printer.printABC(s3, s1); }, "C").start(); }}输出结果:
ABCABCABCABCABCABCABCABCABCABC除了上述五种方法,还有其他方法吗 还有LockSupport、CountDownLatch、AtomicInteger等等。