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

2022-03-22 09:55:03 +08:00
 bjhc

最近在重新学习有关 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]
3445 次点击
所在节点    Java
32 条回复
BiChengfei
2022-03-22 11:41:08 +08:00
@lancelee01 你是这个意思吗,我看了半小时才弄懂
sharping
2022-03-22 11:47:41 +08:00
提供一个思路,你们可以把这个 list 里的 Integer 改成 String 测试下,add 时多添加当前线程名字,list.add(Thread.currentThread().getName().substring(9));
Red998
2022-03-22 11:53:06 +08:00
list 加个 volatile
liangkang1436
2022-03-22 12:19:07 +08:00
你的这个测试代码会最大的问题是会产生死锁,你没发现日志只有 20 行,进程无法结束吗?因为消费者线程运行一次之后进入 blocked 状态,然后,虽然生产者再插入 10 行,也 blocked 了,好了,所有的线程都组赛了,没有人唤醒他们了,即死锁,根本原因很简单,生产者线程和消费者线程不应该使用同一个锁,而是因为各自使用一个锁,生产者发现 list 满了,就阻塞,通知处于消费锁等待序列的消费线程,消费者发现 list 是空也阻塞,通知处于生产锁的等待序列的生产线程,这样,你的程序才能正常轮转
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
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
2022-03-22 12:37:37 +08:00
说错了,看太快看错了,哄堂大笑了家人们,测试代码不会产生死锁,所有的写入线程都阻塞之后,消费线程就会获取对象锁,测试代码是可以正常流转的,最后进程无法结束是生产线程都结束了而消费线程一直阻塞没人唤醒。
回答楼主的问题
一个线程在日志中出现多次原因也很简单,因为你的 synchronized 关键字的范围只保证一个 list 的添加操作是原子性的,并没有保证一个线程连续添加 10 个元素是原子性的,也就是说,list 的 10 个元素,可能有 2 个来自于线程 A ,3 个来自于线程 B ,而日志只会打印添加最后一个元素的线程,所以,list 20 次填满,有一个线程刚好是填入最后一个元素的线程,。很正常
summerLast
2022-03-22 13:04:53 +08:00
核心是每个元素并不是第一个是第一个线程第二个是第二个线程,产生的第 n 个元素并非是 n%20, 线程的执行并不是有序的,所以并不会均匀分布,这里你可以对生成者的 list 进行改造 如 List<DataInfo> DataInfo {threadName,num}再看一下打印结果
summerLast
2022-03-22 13:24:08 +08:00
```
public void run() {
int i = 10;
while (i > 0) {
waitAndNotify.producer();
i--;
}
}
```
i 调大一些
summerLast
2022-03-22 13:49:18 +08:00
为什么 i 调大会发现基本上所有线程都出现的原因 有两个:
- 1.并发一种是硬件上的实现 并行,一种是软件上的实现时间片,而后者既然是时间片就牵扯到上下文切换,如果单个线程执行的周期小于上下文切换的周期,那多线程反而有时不如单线程快,比如 1:4 的关系 1 个时钟周期执行任务 4 个时钟周期执行上下文切换开四个线程反而没有一个线程要快,这也是为什么会看到并不是一个任务切换一次线程的重要原因,也就是线程执行并非是有序的,就不能按有序的思维去看待;
- 2.另一个原因是 如果一个 MyProducer 线程执行完 10 次会出现什么情况,显然并不会再去调用 producer 方法 那既然不调用 producer 方法 log.info 又怎么会打印呢
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
2022-03-23 09:38:40 +08:00
感谢大家

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/842015

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX