一、固定运行顺序
题目:有两个线程分别输出1和2,要求输出结果必须先2后1打印
1.1 wait notify 版
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
| import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test01") public class Test01 { static final Object lock = new Object(); static boolean t2runned = false;
public static void main(String[] args) { Thread t1 = new Thread(() -> { synchronized (lock){ while (!t2runned){ try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("1"); } },"t1");
Thread t2 = new Thread(() -> { synchronized (lock) { log.debug("2"); t2runned = true; lock.notifyAll(); } },"t2");
t1.start(); t2.start(); } }
|
运行结果
1 2
| 20:54:47.396 c.Test01 [t2] - 2 20:54:47.398 c.Test01 [t1] - 1
|
可以看到,实现上很麻烦:
- 首先,需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该 wait;
- 第二,如果有些干扰线程错误地 notify 了 wait 线程,条件不满足时还要重新等待,使用了 while 循环来解决 此问题;
- 最后,唤醒对象上的 wait 线程需要使用
notifyAll
,因为『同步对象』上的等待线程可能不止一个。
1.2 await/signal版本实现
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
| import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock;
@Slf4j(topic = "c.Test06") public class Test06 { public static final ReentrantLock lock = new ReentrantLock(); public static Condition condition = lock.newCondition(); public static boolean t2Runned = false;
public static void main(String[] args) { Thread t1 = new Thread(() -> { lock.lock(); try { while (!t2Runned) { try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("1"); } finally { lock.unlock(); } }, "t1");
Thread t2 = new Thread(() -> { lock.lock(); try { log.debug("2"); t2Runned = true; condition.signal(); } finally { lock.unlock(); } }, "t2");
t1.start(); t2.start(); } }
|
运行结果
1 2
| 21:38:38.463 c.Test06 [t2] - 2 21:38:38.464 c.Test06 [t1] - 1
|
1.3 Park Unpark 版
可以使用 LockSupport
类的 park 和 unpark
来简化上面的题目:
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
| import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
@Slf4j(topic = "c.Test02") public class Test02 { public static void main(String[] args) { Thread t1 = new Thread(() -> { LockSupport.park(); log.debug("1"); }, "t1"); t1.start();
new Thread(() -> { log.debug("2"); LockSupport.unpark(t1); },"t2").start(); } }
|
运行结果
1 2
| 21:33:28.193 c.Test02 [t2] - 2 21:33:28.195 c.Test02 [t1] - 1
|
park 和 unpark 方法比较灵活,他俩谁先调用,谁后调用无所谓。并且是以线程为单位进行『暂停』和『恢复』, 不需要『同步对象』和『运行标记』
二、交替输出
题目:线程1 输出 a 5次, 线程2 输出 b 5次, 线程3 输出 c 5次。现在要求输出 abcabcabcabcabcab
2.1 wait/notify版本
通过设置等待标记 flag 来记录当前拥有锁的是哪个线程, 设置下一个标记,来记录下一个唤醒的该是哪个线程。
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
|
public class Test03 { public static void main(String[] args) { WaitNotify waitNotify = new WaitNotify(1,5); new Thread(() -> { waitNotify.print("a",1,2); }).start(); new Thread(() -> { waitNotify.print("b",2,3); }).start(); new Thread(() -> { waitNotify.print("c",3,1); }).start(); } }
class WaitNotify {
public void print(String str,int waitFlag,int nextFlag){ for (int i = 0; i < loopNumber; i++){ synchronized (this) { while (flag != waitFlag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.print(str); flag = nextFlag; this.notifyAll(); } } } private int flag; private int loopNumber;
public WaitNotify(int flag, int loopNumber) { this.flag = flag; this.loopNumber = loopNumber; } }
|
运行结果
2.2 await/signal版本
由于 ReentrantLock
具有多个条件变量的特性,即多个 WaitSet
休息室,所以可以通过设置让其进入不同的休息室休息来实现输出当前线程的字符串,并唤醒下一个休息室中的线程。这种方法存在虚假唤醒的情况,因为没有做 while 判断,但此例中每个休息室中只有一个线程,因此不存在虚假唤醒的情况。
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
| import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock;
import static site.weiyikai.thread.utils.Sleeper.sleep;
public class Test04 { public static void main(String[] args) { AwaitSignal awaitSignal = new AwaitSignal(5); Condition a = awaitSignal.newCondition(); Condition b = awaitSignal.newCondition(); Condition c = awaitSignal.newCondition();
new Thread(() -> { awaitSignal.print("a",a,b); }).start(); new Thread(() -> { awaitSignal.print("b",b,c); }).start(); new Thread(() -> { awaitSignal.print("c",c,a); }).start();
sleep(1); awaitSignal.lock(); try { System.out.println("开始..."); a.signal(); } finally { awaitSignal.unlock(); } }
}
class AwaitSignal extends ReentrantLock { private int loopNumber;
public AwaitSignal(int loopNumber) { this.loopNumber = loopNumber; }
public void print(String str, Condition current,Condition next) { for (int i = 0; i < loopNumber; i++){ lock(); try { current.await(); System.out.print(str); next.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { unlock(); } } } }
|
运行结果
2.3 park/unpark实现
park和unpark没有对象锁的概念了,停止和恢复线程的运行都是以线程自身为单位的,所以实现更为简单。
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
| import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
@Slf4j(topic = "c.Test05") public class Test05 { static Thread t1; static Thread t2; static Thread t3; public static void main(String[] args) { ParkUnpark unpark = new ParkUnpark(5); t1 = new Thread(() -> { unpark.print("a", t2); }); t2 = new Thread(() -> { unpark.print("b", t3); }); t3 = new Thread(() -> { unpark.print("c", t1); }); t1.start(); t2.start(); t3.start(); LockSupport.unpark(t1); } }
class ParkUnpark { public void print(String str,Thread next){ for (int i = 0; i < loopNumber; i++){ LockSupport.park(); System.out.print(str); LockSupport.unpark(next); } }
private int loopNumber;
public ParkUnpark(int loopNumber) { this.loopNumber = loopNumber; } }
|
运行结果