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

问一个关于 Java 线程的疑惑?

  •  
  •   bjhc · 2022-03-22 09:55:03 +08:00 · 3473 次点击
    这是一个创建于 1012 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近在重新学习有关 java 多线程方面的知识。然后使用内部锁的机制,想简单模拟一下生产者和消费者。 代码逻辑大概是这样的:每个生产者线程产生 10 个数据,然后供一个消费者消费。由于在 main 线程中设置了每个生产者线程的名字,所以想在生产者生成数据的逻辑中打印当前线程名。 我的疑惑:

    1. 为什么打印日志中,线程的名字有重复?比如在日志中没有看到 producer-2 或 producer-5 ,但是 producer-17 和 producer-11 分别重复出现了 3 次?
    2. 线程总数可以对上,不知道是不是代码哪里出了问题还是本身没有理解到位?

    代码如下与日志输出如下:

    package org.example;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    import java.util.Random;
    
    import lombok.SneakyThrows;
    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j
    public class WaitAndNotify {
    
    	private final Object lock = new Object();
    
    	private final List<Integer> list = new ArrayList<>();
    
    	private final Random random = new Random();
    
    	public void producer() throws InterruptedException {
    		synchronized (lock) {
    			while (list.size() == 10) {
    				lock.wait();
    			}
    			list.add(random.nextInt(100));
    			if (list.size() == 10) {
    				log.info("thread-{},{}", Thread.currentThread().getName(),
    						Arrays.toString(list.toArray(new Integer[0])));
    			}
    			lock.notifyAll();
    		}
    	}
    
    	public void consumer() throws InterruptedException {
    		synchronized (lock) {
    			while (list.size() < 10) {
    				lock.wait();
    			}
    			list.clear();
    			lock.notifyAll();
    		}
    	}
    
    	static class MyProducer implements Runnable {
    
    		private final WaitAndNotify waitAndNotify;
    
    		public MyProducer(WaitAndNotify waitAndNotify) {
    			this.waitAndNotify = waitAndNotify;
    		}
    
    		@SneakyThrows
    		@Override
    		public void run() {
    			int i = 10;
    			while (i > 0) {
    				waitAndNotify.producer();
    				i--;
    			}
    		}
    	}
    
    	static class MyConsumer implements Runnable {
    
    		private final WaitAndNotify waitAndNotify;
    
    		public MyConsumer(WaitAndNotify waitAndNotify) {
    			this.waitAndNotify = waitAndNotify;
    		}
    
    		@SneakyThrows
    		@Override
    		public void run() {
    			while (true) {
    				waitAndNotify.consumer();
    			}
    		}
    	}
    
    	public static void main(String[] args) {
    		WaitAndNotify waitAndNotify = new WaitAndNotify();
    		Runnable runnable = new MyProducer(waitAndNotify);
    		for (int i = 0; i < 20; i++) {
    			Thread producer = new Thread(runnable, "producer-" + i);
    			producer.start();
    		}
    		Thread consumer = new Thread(new MyConsumer(waitAndNotify), "consumer");
    		consumer.start();
    	}
    }
    
    
    2022-03-21 17:32:42,014 INFO  [learning-concurrency][producer-0] org.example.ch5._1_1.WaitAndNotify producer(33): thread-producer-0,[49, 96, 0, 43, 23, 6, 79, 44, 28, 21]
    2022-03-21 17:32:42,019 INFO  [learning-concurrency][producer-17] org.example.ch5._1_1.WaitAndNotify producer(33): thread-producer-17,[92, 56, 9, 43, 77, 24, 53, 74, 44, 85]
    2022-03-21 17:32:42,019 INFO  [learning-concurrency][producer-1] org.example.ch5._1_1.WaitAndNotify producer(33): thread-producer-1,[86, 11, 10, 58, 5, 86, 3, 68, 73, 24]
    2022-03-21 17:32:42,020 INFO  [learning-concurrency][producer-17] org.example.ch5._1_1.WaitAndNotify producer(33): thread-producer-17,[10, 9, 97, 41, 81, 35, 84, 0, 86, 12]
    2022-03-21 17:32:42,020 INFO  [learning-concurrency][producer-3] org.example.ch5._1_1.WaitAndNotify producer(33): thread-producer-3,[13, 90, 30, 45, 1, 8, 43, 68, 49, 26]
    2022-03-21 17:32:42,020 INFO  [learning-concurrency][producer-17] org.example.ch5._1_1.WaitAndNotify producer(33): thread-producer-17,[81, 19, 34, 58, 89, 66, 74, 76, 20, 5]
    2022-03-21 17:32:42,021 INFO  [learning-concurrency][producer-4] org.example.ch5._1_1.WaitAndNotify producer(33): thread-producer-4,[48, 60, 0, 36, 97, 9, 42, 44, 67, 81]
    2022-03-21 17:32:42,021 INFO  [learning-concurrency][producer-16] org.example.ch5._1_1.WaitAndNotify producer(33): thread-producer-16,[77, 38, 17, 63, 71, 7, 80, 64, 61, 19]
    2022-03-21 17:32:42,021 INFO  [learning-concurrency][producer-6] org.example.ch5._1_1.WaitAndNotify producer(33): thread-producer-6,[47, 56, 50, 7, 89, 45, 32, 91, 97, 66]
    2022-03-21 17:32:42,022 INFO  [learning-concurrency][producer-16] org.example.ch5._1_1.WaitAndNotify producer(33): thread-producer-16,[27, 96, 24, 85, 3, 28, 23, 21, 93, 72]
    2022-03-21 17:32:42,022 INFO  [learning-concurrency][producer-7] org.example.ch5._1_1.WaitAndNotify producer(33): thread-producer-7,[4, 95, 19, 95, 42, 9, 49, 5, 54, 8]
    2022-03-21 17:32:42,022 INFO  [learning-concurrency][producer-13] org.example.ch5._1_1.WaitAndNotify producer(33): thread-producer-13,[43, 81, 97, 19, 40, 65, 48, 32, 61, 77]
    2022-03-21 17:32:42,023 INFO  [learning-concurrency][producer-14] org.example.ch5._1_1.WaitAndNotify producer(33): thread-producer-14,[67, 81, 84, 11, 73, 71, 65, 26, 78, 45]
    2022-03-21 17:32:42,023 INFO  [learning-concurrency][producer-12] org.example.ch5._1_1.WaitAndNotify producer(33): thread-producer-12,[54, 48, 67, 88, 0, 78, 99, 12, 49, 84]
    2022-03-21 17:32:42,023 INFO  [learning-concurrency][producer-7] org.example.ch5._1_1.WaitAndNotify producer(33): thread-producer-7,[58, 66, 53, 39, 14, 64, 2, 57, 27, 91]
    2022-03-21 17:32:42,023 INFO  [learning-concurrency][producer-11] org.example.ch5._1_1.WaitAndNotify producer(33): thread-producer-11,[91, 79, 70, 36, 34, 28, 63, 67, 17, 77]
    2022-03-21 17:32:42,024 INFO  [learning-concurrency][producer-10] org.example.ch5._1_1.WaitAndNotify producer(33): thread-producer-10,[2, 76, 42, 92, 91, 87, 64, 20, 25, 3]
    2022-03-21 17:32:42,024 INFO  [learning-concurrency][producer-9] org.example.ch5._1_1.WaitAndNotify producer(33): thread-producer-9,[46, 57, 98, 0, 28, 68, 63, 71, 97, 54]
    2022-03-21 17:32:42,024 INFO  [learning-concurrency][producer-11] org.example.ch5._1_1.WaitAndNotify producer(33): thread-producer-11,[26, 66, 10, 77, 69, 41, 77, 80, 21, 55]
    2022-03-21 17:32:42,024 INFO  [learning-concurrency][producer-11] org.example.ch5._1_1.WaitAndNotify producer(33): thread-producer-11,[94, 97, 84, 87, 85, 53, 77, 88, 79, 84]
    
    32 条回复    2022-03-23 09:38:40 +08:00
    312ybj
        1
    312ybj  
       2022-03-22 10:28:17 +08:00
    线程复用了, 你把生产者的数量调整大点,多跑几次
    0o0o0o0
        2
    0o0o0o0  
       2022-03-22 10:30:45 +08:00
    非公平锁?
    pennai
        3
    pennai  
       2022-03-22 10:33:23 +08:00 via Android
    这应该是 monitor lock 是非公平锁的问题,你的哪个生产者可以获得锁是完全随机的,并非你想象的“每个生产者正好可以获得一次锁”。实际情况是有些线程拿了多次锁,有些线程没拿到锁。
    bjhc
        4
    bjhc  
    OP
       2022-03-22 10:42:07 +08:00
    @pennai 虽然是随机的,但是每个线程不应该只执行一次吗?
    bjhc
        5
    bjhc  
    OP
       2022-03-22 10:42:39 +08:00
    @312ybj 调大了也不行,还是有重复的
    lancelee01
        6
    lancelee01  
       2022-03-22 10:45:18 +08:00
    “日志中没有看到 producer-2 或 producer-5 ,但是 producer-17 和 producer-11 分别重复出现了 3 次”。这个是因为元素满 10 个打印,所以打印多次的是正好这个线程每次执行时生成的元素正好是第 10 个元素,没有打印是该线程每次执行生成数对应列表内是 0-8 下标。想看到所有线程应该在 if 判断外。
    bjhc
        7
    bjhc  
    OP
       2022-03-22 10:49:31 +08:00
    @lancelee01 重复出现的线程,打印出的数组元素也不同,这是为什么?
    lancelee01
        8
    lancelee01  
       2022-03-22 10:51:16 +08:00
    @bmwyhc 因为你的消费者在消费呀,打印说明满 10 个元素,生产者阻塞,唤醒消费者
    lancelee01
        9
    lancelee01  
       2022-03-22 10:51:44 +08:00
    哈哈哈,感觉这个代码不是你写的 doge
    bjhc
        10
    bjhc  
    OP
       2022-03-22 10:52:04 +08:00
    @lancelee01 我也发现这个问题了,如果在 if 语句外,可以打印出这 20 个线程,而且这 20 个线程的数组元素最后都符合满 10 的条件。为啥在 if 里就不一样。
    bjhc
        11
    bjhc  
    OP
       2022-03-22 10:53:08 +08:00
    @lancelee01 那不会,当然是自己写的。我就是好奇为什么在条件里和条件外会有不同的输出。
    lancelee01
        12
    lancelee01  
       2022-03-22 10:54:16 +08:00
    @bmwyhc 原因就是我首次回复内容
    bjhc
        13
    bjhc  
    OP
       2022-03-22 10:55:53 +08:00
    @lancelee01 可是最后所有线程都满 10 了呀。
    ffkjjj
        14
    ffkjjj  
       2022-03-22 10:56:26 +08:00
    @bmwyhc #7 并不是同一个线程一个集合, 为啥同线程会打印出一样的元素
    你代码的执行过程是: 所有线程通过竞争锁向同一个集合中添加元素, 当集合元素满 10 个的时候, 此时, 如果 producer 竞争到锁便进入 lock.wait() 等待, 如果是 consumer 竞争到锁, 则清空 list. 然后以此重复.
    bjhc
        15
    bjhc  
    OP
       2022-03-22 10:58:39 +08:00
    @ffkjjj 您的意思我也想到过,但是将 if 语句移除,居然可以打印出所有正确的线程。
    lancelee01
        16
    lancelee01  
       2022-03-22 10:59:05 +08:00
    @bmwyhc 多线程又不是串行执行,waitAndNotify.producer(); 这个是多个线程交替执行的,每个线程只是生产 10 个元素,但不是连续生产,所以完全有可能第一个线程生产了 8 个,然后第二个线程生产了 2 个。然后第二个线程生产 8 个,第一个线程生产 2 个。这样第二个线程有很大几率打印 2 次,比如第一轮的下标 8-9 ,第二轮的 2-9
    ffkjjj
        17
    ffkjjj  
       2022-03-22 11:23:45 +08:00
    @bmwyhc #15 和 if 没关系, 锁机制的问题, synchronized 是非公平锁, 可以了解下 synchronized 对应的 偏向锁, 轻量锁 和重量锁的特点
    bjhc
        18
    bjhc  
    OP
       2022-03-22 11:33:46 +08:00
    @ffkjjj 好的多谢
    ffkjjj
        19
    ffkjjj  
       2022-03-22 11:35:04 +08:00
    @ffkjjj #17
    ~~synchronized 是非公平锁~~(这句话放在这里可能不合适)
    BiChengfei
        20
    BiChengfei  
       2022-03-22 11:35:20 +08:00   ❤️ 1
    @bmwyhc 这个老铁(@lancelee01 )说的是对的
    ---------------------------------------------------------------------------------------
    举个例子:
    你找了 20 个人来搬砖头,每个人总共有 10 块。他们需要把砖头放到同一辆小推车里,当小推车里有 10 块砖头的时候,他们就喊你把小推车拉走,你清空后再把小推车拉回来,他们接着装。
    ** 上述就是你代码的解读 **
    ---------------------------------------------------------------------------------------
    下面是我的理解:
    你的构想,工人应该排队搬砖,0 号开始搬,0 号搬完 1 号搬,一直到 19
    事实上,工人会争先恐后的搬砖,可能有个机灵鬼,每次都等车里有 9 块的时候,就去放最后 1 块,这就导致了每次你都看到他
    ---------------------------------------------------------------------------------------
    如果想做到你想的那样,你应该给工人加锁,而不是给小推车加锁
    ---------------------------------------------------------------------------------------
    验证:你把 producer() 方法中的 if 给删除,然后打印,就像下面这样
    ```
    list.add(random.nextInt(100));
    System.out.println("thread-" + Thread.currentThread().getName() + Arrays.toString(list.toArray()));
    lock.notifyAll();
    ```
    你会看到线程争先恐后的向 list 里面放砖,但每个线程都只有 10 块砖
    BiChengfei
        21
    BiChengfei  
       2022-03-22 11:41:08 +08:00
    @lancelee01 你是这个意思吗,我看了半小时才弄懂
    sharping
        22
    sharping  
       2022-03-22 11:47:41 +08:00
    提供一个思路,你们可以把这个 list 里的 Integer 改成 String 测试下,add 时多添加当前线程名字,list.add(Thread.currentThread().getName().substring(9));
    Red998
        23
    Red998  
       2022-03-22 11:53:06 +08:00
    list 加个 volatile
    liangkang1436
        24
    liangkang1436  
       2022-03-22 12:19:07 +08:00
    你的这个测试代码会最大的问题是会产生死锁,你没发现日志只有 20 行,进程无法结束吗?因为消费者线程运行一次之后进入 blocked 状态,然后,虽然生产者再插入 10 行,也 blocked 了,好了,所有的线程都组赛了,没有人唤醒他们了,即死锁,根本原因很简单,生产者线程和消费者线程不应该使用同一个锁,而是因为各自使用一个锁,生产者发现 list 满了,就阻塞,通知处于消费锁等待序列的消费线程,消费者发现 list 是空也阻塞,通知处于生产锁的等待序列的生产线程,这样,你的程序才能正常轮转
    pennai
        25
    pennai  
       2022-03-22 12:31:55 +08:00
    @bmwyhc 是的,参考 @lancelee01 的说法:”“日志中没有看到 producer-2 或 producer-5 ,但是 producer-17 和 producer-11 分别重复出现了 3 次”。这个是因为元素满 10 个打印,所以打印多次的是正好这个线程每次执行时生成的元素正好是第 10 个元素,没有打印是该线程每次执行生成数对应列表内是 0-8 下标。想看到所有线程应该在 if 判断外。“
    你的 while(i>0){waitNotify.producer();}处,每个线程都在并发执行这段代码,都在竞争 waitNotify 的锁,有的线程竞争到了,而且刚好生产了第十个,所以打印了该线程的名字;有的线程虽然竞争到了锁,但是生产的是前九个,而非第十个所以没有打印名字。
    pennai
        26
    pennai  
       2022-03-22 12:37:16 +08:00
    形象一点说明,如果只有两个线程 A 和 B ,最终会生产出 20 个数据毫无疑问,但是可能出现这样的情况:第 1~4 是线程 A 生产的,第 5~6 是线程 B 生产的,第 7 是线程 A 生产的,第 8~10 是线程 B 生产的,于是打印线程 B 的名字,此时线程 A 生产了 5/10 ,线程 B 生产了 5/10 ;第 11~15 是线程 A 生产的,第 16~20 是线程 B 生产的,于是打印线程 B 的名字。
    liangkang1436
        27
    liangkang1436  
       2022-03-22 12:37:37 +08:00
    说错了,看太快看错了,哄堂大笑了家人们,测试代码不会产生死锁,所有的写入线程都阻塞之后,消费线程就会获取对象锁,测试代码是可以正常流转的,最后进程无法结束是生产线程都结束了而消费线程一直阻塞没人唤醒。
    回答楼主的问题
    一个线程在日志中出现多次原因也很简单,因为你的 synchronized 关键字的范围只保证一个 list 的添加操作是原子性的,并没有保证一个线程连续添加 10 个元素是原子性的,也就是说,list 的 10 个元素,可能有 2 个来自于线程 A ,3 个来自于线程 B ,而日志只会打印添加最后一个元素的线程,所以,list 20 次填满,有一个线程刚好是填入最后一个元素的线程,。很正常
    summerLast
        28
    summerLast  
       2022-03-22 13:04:53 +08:00
    核心是每个元素并不是第一个是第一个线程第二个是第二个线程,产生的第 n 个元素并非是 n%20, 线程的执行并不是有序的,所以并不会均匀分布,这里你可以对生成者的 list 进行改造 如 List<DataInfo> DataInfo {threadName,num}再看一下打印结果
    summerLast
        29
    summerLast  
       2022-03-22 13:24:08 +08:00
    ```
    public void run() {
    int i = 10;
    while (i > 0) {
    waitAndNotify.producer();
    i--;
    }
    }
    ```
    i 调大一些
    summerLast
        30
    summerLast  
       2022-03-22 13:49:18 +08:00
    为什么 i 调大会发现基本上所有线程都出现的原因 有两个:
    - 1.并发一种是硬件上的实现 并行,一种是软件上的实现时间片,而后者既然是时间片就牵扯到上下文切换,如果单个线程执行的周期小于上下文切换的周期,那多线程反而有时不如单线程快,比如 1:4 的关系 1 个时钟周期执行任务 4 个时钟周期执行上下文切换开四个线程反而没有一个线程要快,这也是为什么会看到并不是一个任务切换一次线程的重要原因,也就是线程执行并非是有序的,就不能按有序的思维去看待;
    - 2.另一个原因是 如果一个 MyProducer 线程执行完 10 次会出现什么情况,显然并不会再去调用 producer 方法 那既然不调用 producer 方法 log.info 又怎么会打印呢
    jsdi
        31
    jsdi  
       2022-03-23 01:08:12 +08:00
    ```
    public void producer() throws InterruptedException {
    synchronized (lock) {
    while (list.size() == 10) {
    lock.wait();
    }
    list.add(random.nextInt(100)); //这里出问题了
    if (list.size() == 10) {
    log.info("thread-{},{}", Thread.currentThread().getName(),
    Arrays.toString(list.toArray(new Integer[0])));
    }
    lock.notifyAll();
    }
    }
    ```
    是不是应该把 list.add()放进一个循环中,这样写的话 list 中的 10 个数据可能来自不同线程,而打印什么取决于最后一个是哪个线程放的
    bjhc
        32
    bjhc  
    OP
       2022-03-23 09:38:40 +08:00
    感谢大家
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2826 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 03:00 · PVG 11:00 · LAX 19:00 · JFK 22:00
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.