同步模式之顺序控制(笔试)

一、固定运行顺序

题目:有两个线程分别输出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;

/**
* @author xiaowei
* @date 2022-10-26
* @description 固定运行顺序 wait notify实现
* 必须先2后1打印
**/
@Slf4j(topic = "c.Test01")
public class Test01 {
// 用来同步的对象
static final Object lock = new Object();
//表示 t2 是否运行过
static boolean t2runned = false;

public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock){
// 如果 t2 没有执行过
while (!t2runned){
try {
// t1 先等一会
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("1");
}
},"t1");

Thread t2 = new Thread(() -> {
synchronized (lock) {
log.debug("2");
// 修改运行标记
t2runned = true;
// 通知 obj 上等待的线程(可能有多个,因此需要用 notifyAll)
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;

/**
* @author xiaowei
* @date 2022-10-26
* @description 固定运行顺序 - await/signal版本实现
**/
@Slf4j(topic = "c.Test06")
public class Test06 {
public static final ReentrantLock lock = new ReentrantLock();
public static Condition condition = lock.newCondition();
// t2线程释放执行过
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;

/**
* @author xiaowei
* @date 2022-10-26
* @description 固定运行顺序 Park Unpark实现
**/
@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");
// 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』)
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
/**
* @author xiaowei
* @date 2022-10-26
* @description 交替输出 wait notify实现
**/
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();
}
}

/*
输出内容 等待标记 下一个标记
a 1 2
b 2 3
c 3 1
*/
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;
}
}

运行结果

1
abcabcabcabcabc

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;

/**
* @author xiaowei
* @date 2022-10-26 交替输出 await/signal
* @description
**/
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;
}

/**
* 打印
* @param str 打印内容
* @param current 进入哪一间休息室
* @param next 下一间休息室
*/
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();
}
}
}
}

运行结果

1
2
开始...
abcabcabcabcabc

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;

/**
* @author xiaowei
* @date 2022-10-26
* @description 交替输出 Park/Unpark
**/
@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();
//主线程先唤醒t1
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;
}
}

运行结果

1
abcabcabcabcabc

同步模式之顺序控制(笔试)
http://example.com/2023/03/28/同步模式之顺序控制(笔试)/
作者
程序员小魏
发布于
2023年3月28日
许可协议