1、按序打印
原地址:leetcode-cn.com/problems/pr…
1-1、题目
我们提供一个类,
public class Foo {
public void first() { print("first"); }
public void second() { print("second"); }
public void third() { print("third"); }
}
【三个不同的线程】将会【共用】一个 Foo 实例。
- 线程 A 将会调用 first() 方法 ;
- 线程 B 将会调用 second() 方法 ;
- 线程 C 将会调用 third() 方法 ;
请设计修改程序,以确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。
1-2、题目分析
- 【线程顺序】执行,一个线程等待另一个线程执行完成;
- 与线程创建顺序无关;
- 编码拦中给的代码,每个方法抛出【InterruptedException】异常,就会让人联想到Java中会抛出【InterruptedException】异常的方法,然后看看这些方法是不是可以解决;
1-3、结合Java提供的同步工具分析
1、Thread.sleep
最简单也最容易想到的就是【Thread.sleep】方法,这个方法虽然可以执行,但是【提交】后会【超出时间限制】,因此这个方法不被考虑;也不建议大家在平时工作中使用这个方法,因为Java提供了很多好的同步工具类;
2、synchronized + 条件变量
使用 synchronized + 条件判断变量,比如【first完成】和【second完成】,只有当【first】完成后,second才能执行,当second完成,third可以执行;
类中定义一个锁变量,当进入synchronized代码块时,必须先获取锁,此时做条件判断(注意,wait的条件判断必须是在循环中),当条件不满足时,调用【wait】方法,此时线程会释放锁,等待被唤醒后才能继续执行;
3、CountDownLatch
CountDownLatch设计目的,就是保证【一个线程】等待【另一个线程】执行完后继续处理,例如,给 first 的 CountDownLatch设置倒数为1,second等待 first的 CountDownLatch,当countDown后为0,second继续执行;
CountDownLatch的【计数】不能重置,到0就结束;把 CountDownLatch 的【countDown()】 和【awati()】方法配合使用,就可以完成【线程顺序执行】的功能;
A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.
4、CyclicBarrier
CyclicBarrier也是一组线程相互等待的实现,只是与CountDownLatch不同,CyclicBarrier的【计数】可以通过【reset()】进行重置;
例如,设计两个CyclicBarrier,first打印完成,第一个CyclicBarrier等待,接着在second打印前也 加上 第一个CyclicBarrier 的等待;这里需要注意,CyclicBarrier的await方法除了会抛出 InterruptedException之外,还会抛出【BrokenBarrierException】异常,所以在代码中需要catch这个异常;
A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point. CyclicBarriers are useful in programs involving a fixed sized party of threads that must occasionally wait for each other. The barrier is called cyclic because it can be re-used after the waiting threads are released.
5、Semaphore
Semaphore作为信号量的实现,在Java中对Semaphore有这样的说明,参考【Java内存模型】的【Happens-Before原则】中对于【管程锁定原则】的实现;所以只要在 first 方法中【release】信号量,在 second 方法中 【acquire】 同一个信号量,也能实现顺序执行;
Memory consistency effects: Actions in a thread prior to calling a "release" method such as release() happen-before actions following a successful "acquire" method such as acquire() in another thread.
关于【管程锁定原则】:
- 在监视器锁上的【解锁】操作必须在【同一个监视器锁】的【加锁】操作之前执行;
6、无锁方案
无锁方法就是使用原子类进行判断,类似条件判断,但是没有锁,比如当原子类值AtomicInteger 不等于 1(类似 synchronized + 条件变量 判断),另一个方法不能执行,用这个方法完成线程顺序执行;
1-4、编码实现
1-4-1、CountDownLatch
class Foo {
private CountDownLatch firstLatch;
private CountDownLatch secondLatch;
public Foo() {
firstLatch = new CountDownLatch(1);
secondLatch = new CountDownLatch(1);
}
public void first(Runnable printFirst) throws InterruptedException {
printFirst.run();
// first执行完,firstLatch进行countDown,此时count=0,await的线程收到后进行执行
firstLatch.countDown();
}
public void second(Runnable printSecond) throws InterruptedException {
// 等待firstLatch结束
firstLatch.await();
printSecond.run();
// second执行完,secondLatch进行countDown,此时count=0,await的线程收到后进行执行
secondLatch.countDown();
}
public void third(Runnable printThird) throws InterruptedException {
// 等待secondLatch结束
secondLatch.await();
printThird.run();
}
}
1-4-2、CyclicBarrier
class Foo {
private CyclicBarrier firstBarrier;
private CyclicBarrier secondBarrier;
public Foo() {
firstBarrier = new CyclicBarrier(2);
secondBarrier = new CyclicBarrier(2);
}
public void first(Runnable printFirst) throws InterruptedException {
printFirst.run();
try {
firstBarrier.await();
} catch (BrokenBarrierException e) {
}
}
public void second(Runnable printSecond) throws InterruptedException {
try {
firstBarrier.await();
} catch (BrokenBarrierException e) {
}
printSecond.run();
try {
secondBarrier.await();
} catch (BrokenBarrierException e) {
}
}
public void third(Runnable printThird) throws InterruptedException {
try {
secondBarrier.await();
} catch (BrokenBarrierException e) {
}
printThird.run();
}
}
1-4-3、Semaphore
class Foo {
private Semaphore secondSemaphore = new Semaphore(0);
private Semaphore thirdSemaphore = new Semaphore(0);
public Foo() {
}
public void first(Runnable printFirst) throws InterruptedException {
printFirst.run();
// 释放信号量
secondSemaphore.release();
}
public void second(Runnable printSecond) throws InterruptedException {
// 获取信号量,符合Happens-Before原则
secondSemaphore.acquire();
printSecond.run();
thirdSemaphore.release();
}
public void third(Runnable printThird) throws InterruptedException {
thirdSemaphore.acquire();
printThird.run();
}
}
1-4-4、synchronized + 条件变量
class Foo {
private Object lock;
private boolean firstFinished;
private boolean secondFinished;
public Foo() {
lock = new Object();
firstFinished = false;
secondFinished = false;
}
public void first(Runnable printFirst) throws InterruptedException {
synchronized(lock) {
printFirst.run();
firstFinished = true;
lock.notifyAll();
}
}
public void second(Runnable printSecond) throws InterruptedException {
synchronized(lock) {
while (!firstFinished) {
lock.wait();
}
printSecond.run();
secondFinished = true;
lock.notifyAll();
}
}
public void third(Runnable printThird) throws InterruptedException {
synchronized(lock) {
while (!secondFinished) {
lock.wait();
}
printThird.run();
lock.notifyAll();
}
}
}
1-4-5、无锁变量
class Foo {
private AtomicInteger firstDone;
private AtomicInteger secondDone;
public Foo() {
firstDone = new AtomicInteger(0);
secondDone = new AtomicInteger(0);
}
public void first(Runnable printFirst) throws InterruptedException {
printFirst.run();
firstDone.incrementAndGet();
}
public void second(Runnable printSecond) throws InterruptedException {
while (firstDone.get() != 1) {
}
printSecond.run();
secondDone.incrementAndGet();
}
public void third(Runnable printThird) throws InterruptedException {
while (secondDone.get() != 1) {
}
printThird.run();
}
}
2、两个线程交替打印方法
原题地址:leetcode-cn.com/problems/pr…
2-1、题目
我们提供一个类,
class FooBar {
public void foo() {
for (int i = 0; i < n; i++) {
print("foo");
}
}
public void bar() {
for (int i = 0; i < n; i++) {
print("bar");
}
}
}
【两个不同的线程】将会共用一个 FooBar 实例。其中一个线程将会调用 foo() 方法,另一个线程将会调用 bar() 方法。 请设计修改程序,以确保 "foobar" 被输出 n 次。
示例 1:
输入: n = 1;
输出: "foobar";
解释: 这里有两个线程被异步启动。其中一个调用 foo() 方法, 另一个调用 bar() 方法,"foobar" 将被输出一次。
示例 2:
输入: n = 2
输出: "foobarfoobar"
解释: "foobar" 将被输出两次。
2-2、解题思路
1、最简单的方法就是在方法中根据【计数n】,调用线程Thread的sleep方法,执行foo方法,sleep时间为 2n ,执行 bar方法 sleep时间为 2n + 1,类似 单双数 交替执行,但是这种方法容易导致超时,所以忽略;
2、回到题目,要求是【两个线程】,先后执行foo和bar方法,联想到【等待-通知】机制,给定一个条件值,当【条件满足】调用foo,当【条件不满足】调用bar,因为是在循环中调用,那么调用一次,条件值切换,最简单的使用一个 【boolean 值】 作条件判断;实现代码:
这里需要对【原题的代码】进行修改,将 【i++】循环控制变量,放到for中,否则第一次执行完,就会执行 循环控制语句,这样【当条件满足后】进入循环,boolean已经被修改,无法继续执行,如果要求不改动代码,这个实现不满足;这样就该就类似将【for循环】改成【while循环】;
public class FooBar {
private int n;
private boolean fooPrint = true;
public FooBar(int n) {
this.n = n;
}
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n;) {
if (fooPrint) {
printFoo.run();
fooPrint = false;
i++;
}
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; ) {
if (!fooPrint) {
printBar.run();
fooPrint = true; // 不能在for中执行
i++;
}
}
}
}
3、如果不能更新题目本身的代码,意味着【不能用boolean条件变量,作为方法执行】根据,那么只能通过其他手段实现类似【等待-通知】机制,Java提供的阻塞锁和同步器如下:
ReentrantLock,类似 synchronized的互斥锁,不能实现这个功能;
CountDownLatch,适用于类似【倒计时】的计数器执行完成,比如 foo 需要执行N次,等foo执行完成后,在处理后续操作,不适合用这种交替执行场景;虽然使用CountDownLatch可以实现,但是需要修改代码(假如要求不能修改代码,CountDownLatch 和 Lock都不太适合);
CyclicBarrier,
Semaphore,作为信号量,完全可以实现【先后执行】的操作,执行foo方法时,【acquire】信号量fooSemaphore,当foo执行完成,【release】信号量barSemaphore;
2-3、Java代码实现
2-3-1、用Semaphore实现
class FooBar {
private int n;
private Semaphore fooSemaphore;
private Semaphore barSemaphore;
public FooBar(int n) {
this.n = n;
this.fooSemaphore = new Semaphore(1);
this.barSemaphore = new Semaphore(0);
}
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n; i++) {
fooSemaphore.acquire();
printFoo.run();
barSemaphore.release();
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; i++) {
barSemaphore.acquire();
printBar.run();
fooSemaphore.release();
}
}
}