要求是 3 个线程按顺序打印 abcabc
代码如下:
public class ThreadPrint {
static int sign = 0;
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread(()->{
for (int i = 0; i < 5; ) {
lock.lock();
if (sign % 3 == 0){
System.out.print("a");
sign++;
i++;
}
lock.unlock();
}
}).start();
new Thread(()->{
for (int i = 0; i < 5;) {
lock.lock();
if (sign % 3 == 1){
System.out.print("b");
sign++;
i++;
}
lock.unlock();
}
}).start();
new Thread(()->{
for (int i = 0; i < 5; ) {
lock.lock();
if (sign % 3 == 2){
System.out.print("c");
sign++;
i++;
}
lock.unlock();
}
}).start();
}
}
为什么这能按照顺序打印出来呢?
第一个线程 unlock 之后为什么不能 for 循环继续 lock 呢?
继续 lock 的话就不能打印出 5 个 a 了。
然而打印结果是 5 个按顺序的 abc
1
Jooooooooo 2021-12-29 14:53:49 +08:00
这能打印出 abc 感觉是运气好...
|
2
wolfie 2021-12-29 15:02:22 +08:00
这个不就是 用 sign + i 控制,死循环 硬打 abc
|
3
falsemask 2021-12-29 15:05:43 +08:00
代码没啥问题吧,sign++操作都是锁的范围里,是线程安全的,所有一个线程拿到锁之后判断 sign % 3,不满足条件就会释放锁,满足 sign % 3 条件的线程一定会被唤醒,所以还是按 abc 顺序输出五次
|
5
final7genesis 2021-12-29 15:07:12 +08:00
第一个线程有可能继续 lock ,但是 Lock 后 sign 值是 1 啊, 不满足 if 条件, 相当于无效循环, 又释放了 lock
|
6
zheng96 2021-12-29 15:07:31 +08:00
for 循环里没有对 i 进行增加,i++放到了 sign%3==0 判断里,所以其他时间是在死循环 i<5 ,直到下一个 sign 符合判断的时候
|
7
dooonabe 2021-12-29 15:10:35 +08:00
虽然 sign 没有被声明为 volatile ,但 sign 是在独占锁的范围内发生变化的,所以每个线程都能看到 sign 的正确值
|
8
gosidealone OP |
9
goalidea 2021-12-29 15:42:59 +08:00
实质是靠抢锁来控制 sign++,同时靠 sign 数值保证 i++以达到循环的目的。说实话没有必要,很浪费 cpu 资源,当被不满足 if 块的线程抢到锁,线程就只是空加锁解锁。你的需求应该是线程通信,简单的用 synchronized 加上 wait ,复杂的试用 Lock 和 Condition
|
10
AlexLokhart 2021-12-29 15:55:05 +08:00
线程通信用 synchronousQueue ,只有当上一个线程 set 值后,下一个线程才能继续,满足这题; ReentrantLock 默认非公平锁,你这么玩纯粹是运气好才按顺序,然而即使是公平锁,线程 start() 的时机仍然不是你控制的,意味着 lock(),unlock() 的时间点你不能控制,顺序也就无从保证。
|
11
jorneyr 2021-12-29 16:33:38 +08:00
最简单的方案是使用 3 个 Semaphore ,第一个线程输出后释放下一个线程的 Semaphore 并且再申请自己的 Semphore 一个资源进行阻塞。
|
12
uCharles 2021-12-29 16:35:01 +08:00
看到线程我的脑袋就疼。。。。
|
13
gosidealone OP @AlexLokhart 不是哦,这不是运气好才按顺序的
|
14
gosansam 2021-12-29 17:39:57 +08:00
虽然能按顺序打印 5 次 abc ,但是每个线程获取到锁的次数有可能不一样,这里靠 lock 更新 sign 和循环的 i 的值,初始的时候即使第二个或者第三个线程抢到了锁,也不会影响 sign 值,只有第一个线程抢到了才会输出 a 、更新 sign==1 和自己的 i==1 ,这时只有第二个线程获取到锁才会输出 b 、更新 sign==2 和自己的 i==1 ,同理到 sign==3 时,又只有第一个线程获取到锁才会更新,虽然结果是 5 个 abc ,但如果每次都是不对应的线程获得倒锁,每次执行的时间都不同
|
15
dejavuwind 2021-12-30 10:41:00 +08:00
反正就是如果 sign 值不符合条件,就算抢到了锁也不给打印,轮到你了才能打印、sign++
|
16
leegoo 2021-12-30 13:43:37 +08:00
我将你这部分代码放到 IDEA 里面,用 JAVC 编译。 发现 for 循环是这样的。
编译前: for (int i = 0; i < 5; ) { lock.lock(); if (sign % 3 == 0){ System.out.print("a"); sign++; i++; } lock.unlock(); } 编译后: for (int i = 0; i < 5; lock.unlock();) { for(语句 A; 语句 B; 语句 C){ 语句 A 在整个循环过程中,只会执行一次;语句 B 必须是布尔类型的表达式(当然也可以不写,如果写就必须是布尔类型表达式),通过该布尔表达式去判断是否继续执行循环体;语句 C 会在每次循环结束后执行,也就是说,循环体执行多少次,语句 C 就会执行多少次。(抄自 https://www.jb51.net/article/157807.htm ) 根据编译后+jb 网站的猜测。当 A 线程获取到锁之后。B 线程如果需要再获取锁,肯定是需要 A 线程释放锁,B 才有机会的。 但是我的问题是: 1.不管语句 C 是什么情况: 只要有语句 B 返回的是布尔值。 第一次肯定会触发一次循环体的。 那么为什么不管怎么样都是先打印 a 而不是先打印 b or c 2.后续我将 for 循环改成普通的模式 for (int i = 0; i < 5; i++) { lock 变量改为 static volatile ReentrantLock lock = new ReentrantLock(); sign 改为 static volatile AtomicInteger sign = new AtomicInteger(0); 发现只会打印一次 abc 但是依然无法理解为什么一定是打印 abc 不是 acb cba 等 |