V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
gosidealone
V2EX  ›  Java

有没有大佬看下 Java 多线程的问题

  •  
  •   gosidealone · 2021-12-29 14:48:28 +08:00 · 3015 次点击
    这是一个创建于 1039 天前的主题,其中的信息可能已经有所发展或是发生改变。

    要求是 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

    16 条回复    2021-12-30 13:43:37 +08:00
    Jooooooooo
        1
    Jooooooooo  
       2021-12-29 14:53:49 +08:00
    这能打印出 abc 感觉是运气好...
    wolfie
        2
    wolfie  
       2021-12-29 15:02:22 +08:00
    这个不就是 用 sign + i 控制,死循环 硬打 abc
    falsemask
        3
    falsemask  
       2021-12-29 15:05:43 +08:00
    代码没啥问题吧,sign++操作都是锁的范围里,是线程安全的,所有一个线程拿到锁之后判断 sign % 3,不满足条件就会释放锁,满足 sign % 3 条件的线程一定会被唤醒,所以还是按 abc 顺序输出五次
    falsemask
        4
    falsemask  
       2021-12-29 15:06:15 +08:00
    @falsemask typo ,所有=所以
    final7genesis
        5
    final7genesis  
       2021-12-29 15:07:12 +08:00
    第一个线程有可能继续 lock ,但是 Lock 后 sign 值是 1 啊, 不满足 if 条件, 相当于无效循环, 又释放了 lock
    zheng96
        6
    zheng96  
       2021-12-29 15:07:31 +08:00
    for 循环里没有对 i 进行增加,i++放到了 sign%3==0 判断里,所以其他时间是在死循环 i<5 ,直到下一个 sign 符合判断的时候
    dooonabe
        7
    dooonabe  
       2021-12-29 15:10:35 +08:00
    虽然 sign 没有被声明为 volatile ,但 sign 是在独占锁的范围内发生变化的,所以每个线程都能看到 sign 的正确值
    gosidealone
        8
    gosidealone  
    OP
       2021-12-29 15:11:22 +08:00
    @zheng96
    @final7genesis
    @falsemask 明白了。。。 谢谢各位
    goalidea
        9
    goalidea  
       2021-12-29 15:42:59 +08:00
    实质是靠抢锁来控制 sign++,同时靠 sign 数值保证 i++以达到循环的目的。说实话没有必要,很浪费 cpu 资源,当被不满足 if 块的线程抢到锁,线程就只是空加锁解锁。你的需求应该是线程通信,简单的用 synchronized 加上 wait ,复杂的试用 Lock 和 Condition
    AlexLokhart
        10
    AlexLokhart  
       2021-12-29 15:55:05 +08:00
    线程通信用 synchronousQueue ,只有当上一个线程 set 值后,下一个线程才能继续,满足这题; ReentrantLock 默认非公平锁,你这么玩纯粹是运气好才按顺序,然而即使是公平锁,线程 start() 的时机仍然不是你控制的,意味着 lock(),unlock() 的时间点你不能控制,顺序也就无从保证。
    jorneyr
        11
    jorneyr  
       2021-12-29 16:33:38 +08:00
    最简单的方案是使用 3 个 Semaphore ,第一个线程输出后释放下一个线程的 Semaphore 并且再申请自己的 Semphore 一个资源进行阻塞。
    uCharles
        12
    uCharles  
       2021-12-29 16:35:01 +08:00
    看到线程我的脑袋就疼。。。。
    gosidealone
        13
    gosidealone  
    OP
       2021-12-29 16:50:02 +08:00
    @AlexLokhart 不是哦,这不是运气好才按顺序的
    gosansam
        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 ,但如果每次都是不对应的线程获得倒锁,每次执行的时间都不同
    dejavuwind
        15
    dejavuwind  
       2021-12-30 10:41:00 +08:00
    反正就是如果 sign 值不符合条件,就算抢到了锁也不给打印,轮到你了才能打印、sign++
    leegoo
        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 等
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   946 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 927ms · UTC 20:32 · PVG 04:32 · LAX 13:32 · JFK 16:32
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.