零基础学 Java
臧萌
PayPal 数据处理组技术负责人
46665 人已学习
新⼈⾸单¥68
课程目录
已完结/共 170 讲
第二章 Java面向对象编程 (74讲)
时长 07:13
时长 12:08
时长 06:26
时长 05:30
时长 14:16
时长 08:30
零基础学 Java
登录|注册
留言
11
收藏
沉浸
阅读
分享
手机端
回顶部
当前播放: 142 | 多线程经典模型:生产者消费者
00:00 / 00:00
高清
  • 高清
1.0x
  • 2.0x
  • 1.5x
  • 1.25x
  • 1.0x
  • 0.75x
  • 0.5x
网页全屏
全屏
00:00
付费课程,可试看
01 | 课程介绍
02 | 内容综述
03 | 开发环境搭建(macOS)
04 | HelloWorld程序编译和运行(macOS)
05 | 开发环境搭建(Windows)
06 | HelloWorld程序编译和运行(Windows)
07 | 详解HelloWorld程序
08 | IntelliJ IDEA集成开发环境的安装和使用(macOS)
09 | IntelliJ IDEA集成开发环境的安装和使用(Windows)
10 | 从加减乘除到变量
11 | 再探计算加减乘除的程序
12 | Java中的基本数据类型
13 | Java中的运算符
14 | Java中的位运算符
15 | 基本数据类型的更多语法点
16 | 字符集编码和字符串
17 | 操作符和数据类型总结
18 | 程序执行流程之if-else语句(上)
19 | 程序执行流程之if-else语句(下)
20 | 程序循环之for语句
21 | 代码块和变量的作用域
22 | 程序循环之while语句
23 | 程序执行流程之switch语句
24 | 循环和判断的总结(上)
25 | 循环和判断的总结(下)
26 | 用数组保存成绩
27 | 认识变量和数组(上)
28 | 认识变量和数组(下)
29 | 多维数组
30 | 用数组灵活处理程序
31 | 类(class)
32 | 初探类和对象
33 | 认识引用类型(上)
34 | 认识引用类型(下)
35 | 类、对象和引用的关系
36 | 认识数组类型
37 | 引用的缺省值null
38 | 像自定义类型一样使用类
39 | Java中的包和访问修饰符(上)
40 | Java中的包和访问修饰符(下)
41 | 打造一个小超市
42 | IntelliJ调试程序初探
43 | 方法:让Merchandise对象有行为
44 | 返回值:让Merchandise计算毛利润
45 | 参数:让Merchandise计算多件商品的总价
46 | 参数和返回值是怎么传递的
47 | 分清参数、局部变量和实例的地盘
48 | 隐藏的this自引用
49 | 理解方法:一种特殊的代码块
50 | 理解方法的调用:代码的一种特殊跳转
51 | 给类和方法加Java注释
52 | 成熟的类的对象要自己做事情
53 | 方法的签名和重载
54 | 重载的参数匹配规则
55 | 构造方法:构造实例的方法
56 | 构造方法的重载和互相调用
57 | 静态变量
58 | 静态方法
59 | 静态方法的重载
60 | static代码块和static变量初始化
61 | 方法和属性的可见性修饰符
62 | 重新认识老朋友:Math和Scanner(上)
63 | 重新认识老朋友:Math和Scanner(下)
64 | 最熟悉的陌生人:String (上)
65 | 最熟悉的陌生人:String (下)
66 | 重新认识老朋友: main方法和System类
67 | String类的好兄弟
68 | 继承:方便让商品增加新的类别
69 | 子类对象里藏着一个父类对象
70 | 覆盖:子类想要一点不一样
71 | super:和父类对象沟通的桥梁
72 | super:调用父类的构造方法
73 | 父类和子类的引用赋值关系
74 | 多态:到底调用的哪个方法?(上)
75 | 多态:到底调用的哪个方法?(下)
76 | 多态里更多的语法点(上)
77 | 多态里更多的语法点(下)
78 | instanceof操作符
79 | 继承专属的访问控制:protected
80 | final修饰符(上)
81 | final修饰符(下)
82 | 继承里的静态方法
83 | 插曲:for循环的另一种写法
84 | 万类之祖:Object类
85 | hashCode和equals 方法(上)
86 | hashCode和equals 方法(下)
87 | toString方法
88 | 初探Class类
89 | 初探反射(上)
90 | 初探反射(下)
91 | 面向对象三要素:封装、继承和多态
92 | 枚举:定义商品的门类
93 | 接口:让商品类型更丰富(上)
94 | 接口:让商品类型更丰富(下)
95 | 抽象类:接口和类的混合体
96 | 有方法代码的接口
97 | 接口内代码的更多内容
98 | 静态内部类
99 | 成员内部类
100 | 局部内部类
101 | 匿名类
102 | 特殊类的总结
103 | 让我们的超市运转起来:设计篇
104 | 让我们的超市运转起来:代码篇
105 | 初识异常:try catch
106 | Java中异常的分类
107 | 抛出异常的语法
108 | Java异常的传递
109 | 自定义异常
110 | 异常传递不是凌波微步
111 | try catch finally语句
112 | 自动回收资源的try语句
113 | Java中的常见异常
114 | Collection类族简介
115 | Collection中的List (上)
116 | Collection中的List(下)
117 | Collection中的Set
118 | 泛型简析(上)
119 | 泛型简析(下)
120 | 再探泛型
121 | Iterator接口
122 | Map:key和value的映射
123 | 定义自己的注解
124 | Lambda V.S. 匿名类(上)
125 | Lambda V.S. 匿名类(下)
126 | 基本类型的自动装箱和拆箱
127 | Java中的File类
128 | Java I/O简介
129 | 写文件内容小程序
130 | 读文件内容小程序
131 | 网络通讯名词简介
132 | 简单的网络通讯小程序(上)
133 | 简单的网络通讯小程序(下)
134 | 简单的抓取网页内容的程序
135 | JDK和JRE
136 | 初识线程
137 | 创建自己的线程
138 | 再探线程
139 | 多线程:混乱开始了
140 | 同步控制之synchronized
141 | 同步控制之wait notify
142 | 多线程经典模型:生产者消费者
143 | 线程同步之join
144 | 死锁
145 | ThreadLocal线程专属的变量
146 | 定时任务
147 | volatile关键字的作用
148 | concurrent包基本原理
149 | concurrent包中的Atomic类族
150 | concurrent包中的锁
151 | concurrent包中的数据结构
152 | concurrent包中的线程池
153 | 聊天室开张喽 (上)
154 | 聊天室开张喽 (下)
155 | 什么是学习一门语言
156 | Java平台简介
157 | Maven概念简介
158 | Maven的安装和配置
159 | 创建一个简单的Maven项目
160 | 一个从pptx文件中抽取文字的小工具
161 | Maven常用命令和插件
162 | Intellij更多功能介绍
163 | 值得学习的类库简介
164 | 如何在Stack Overflow上提问才不会被骂
165 | 浅谈程序设计
166 | 游戏小程序功能定义
167 | 游戏小程序设计和模块划分
168 | 游戏小程序代码分析
169 | 使用Swagger创建一个Spring Boot的Web服务
170 | 结课测试&结束语
本节摘要

PDF 课件和源代码下载地址:
https://gitee.com/geektime-geekbang/LetsJava

登录 后留言

全部留言(11)

  • 最新
  • 精选
Geek_4e68bc
刚刚被一个问题卡住了,如果consumer执行notify之后释放了锁,那还怎么return 呀?后面查了一下才知道,notify()或者notifyAll()调用时并不会真正释放对象锁, 必须等到synchronized方法或者语法块执行完才真正释放锁。

作者回复: 会释放锁,但是因为后面的代码依旧在synchronized块里,如果想继续执行,必须再次抢到锁

2022-03-15
1
朱家华
写一下代码中老师提出的问题1:while换成if会怎么样 public T consume() throws InterruptedException { synchronized (tasks) { // 如果不用while用if,会怎么样? if (tasks.size() == 0) { System.out.println("消费者线程进入等待: " + Thread.currentThread().getName()); tasks.wait(); } T ret = tasks.poll(); tasks.notifyAll(); return ret; } } 如果consumer-1由于队列空了释放锁,之后consumer-2抢到了锁也因为队列空了释放锁,之后consumer-3抢到了锁也因为队列空了释放了锁。之后producer-1写入了数据,进而notifyAll()唤醒了所有线程。之后conusmer-1抢到了锁,并从wait()后开始执行,把唯一的数据消费出来并处理,之后consumer-2抢到了锁,由于其从wait()之后开始执行,且这里是if,所以其不再循环,不再判断队列中是否有数据,而是继续向下执行。那么就会出问题。 发生这个问题的本质是: 在多线程的环境下,线程进入WAITING状态前和被唤醒后,线程所处的环境是随机的,谁都不知道多个consumer和多个producer运行过后,队列中的数据究竟是什么样的,队列中的情况是随机的。而如果wait()的调用如果被if包裹,那么就只会执行一次,即调用wait()前检查,但是被唤醒后就不再检查了。也就是说,我们放任consumer-2线程被唤醒后不对队列中的状态进行检查,就继续进行消费动作。 所以得到的结论是: 如果wait()的调用是基于某些条件才调用的,即在特定条件下要让线程进入WAITING状态,那么一定要用while()包括wait()的调用,以便在被唤醒后仍然可以再次进行条件检查。 问题2:如果while检查在synchronized代码块外面: public void produce(T task) throws InterruptedException { while (tasks.size() >= maxTaskCount) { synchronized (tasks) { System.out.println("生产者线程进入等待: " + Thread.currentThread().getName()); tasks.wait(); } } tasks.add(task); tasks.notifyAll(); } 比如下述代码不是基于现在的producer和consumer情景,只是多个线程同时操作一个对象,执行的操作都一样。只是单纯的竞争锁,不区分写和读。 那么就会出现这种情况,所有线程满足条件的时候,就会一个一个进入到synchronized代码块,然后纷纷进入WAITING状态。那么就没人唤醒其他线程了。 这种问题的本质在于: 所有线程可以同时执行到while的条件判断,假设这发生在A时刻,那么此时所有线程都判断要进入WAITING状态,然后thread-1抢到锁,进入WAITING状态,之后thread-2抢到锁,进入WAITING状态,之后thread-3...... 但其实thread-2应该在其抢到锁的时候再去判断此时此刻,是否满足要进入WAITING状态的条件。而不是先在时刻A进行判断,然后抢到锁就进入WAITING。 所以线程要抢到锁再判断是否满足进入WAITING状态的条件。这样的判断是基于当下的,而不是之前某时刻的判断。 所以得到的结论是: 如果要基于某个条件判断是否要让线程进入WAITING状态,那么要让判断条件在synchronized代码块中,这样做的判断才是基于当下进行的判断。

作者回复: 对头,多线程就是乱。synchronized是我想静静

2021-05-26
1
泽昊
他这个用tasks做锁那能不能直接用this做锁呢

作者回复: 也是可以的,锁嘛,只要需要锁的地方用的是同一个对象就可以。

2023-02-07
2
朱家华
部分2: public class AppMain { public static void main(String[] args) { Queue<String> tasks = new LinkedList<>(); Thread conusmer = new Thread(new Consumer<String>(tasks), "conusmer"); Thread producer = new Thread(new Producer<String>(tasks, 1024), "producer"); conusmer.start(); producer.start(); } } 基于老师的代码例子,我发现了一个问题,producer-1执行完后可能是consumer-1抢到锁执行,也可能是另外一个producer-2抢到锁执行。 但是我想实现的例子是这样的情况,我在producer中输入一行字符串到队列,然后让立刻一个conusmer线程消费并打印。consumer消费完后,立即WAIT,并让producer继续往队列里写字符串,之后再次激活consumer消费。也就是说producer和consumer你执行一次我执行一次。 另外,我把制造数据和往队列里放这两个行为都放到了producer里,把处理数据和从队列里拿数据两个行为都放到了consumer里。 问题1: 我在producer和consumer的run方法中,都是这样的执行顺序: 1.执行往队列里写数据或者从队列里拿数据的操作 2.之后notify()处于WAITING状态的所有线程 3.之后让拿到锁的线程进入WAITING状态 运行后发现达到了目的,效果还不错,但多线程的执行顺序太乱,我不敢保证100%不会出问题,所以麻烦老师看一下,这样的调用有没有问题?会不会发生前面说的那种notify()在wait()之前调用的情况? 2.问题2: 上面Consumer的代码中,有两个while判断,其中while(true)的while和synchronized可以互换位置吗? 我自己感觉是可以的,因为while的条件始终都是true,且while执行的代码和synchronized要执行的主要的代码没有区别。而且自己也没有想出来在什么执行次序下回发生问题。老师觉得可以互换位置吗? 3.学习多线程到现在的感受: 最大的问题就是多个线程的执行顺序是任意的,天知道上一次或下一次会怎么执行,就感觉心虚,没有不带多线程的代码那样指哪打哪的感觉。老师有没有什么使用多线程的窍门?

作者回复: 1)建议用notifyAll。至于会不会有notify在wait前面,只要是大家在循环不停的wait和notifyall,应该就不会有死锁 2)在你上个问题里回答啦 3)使用多线程让的窍门就是尽量不使用。如果要使用,就把线程运行模型理清楚:哪批线程干了什么事,之间怎么同步的。当然,最好是不要用,别玩太花的技巧😄

2021-05-26
朱家华
老师,我自己重新实现了消费者生产者的代码,还是字数有点多,我把内容分成了两部分, 部分1如下: public class Consumer<T> implements Runnable{ private Queue<T> tasks; public Consumer(Queue<T> tasks) { this.tasks = tasks; } private void process(T task) throws InterruptedException { System.out.println("开始处理..."); System.out.println("输入的字符串为:" + task); Thread.sleep(TimeUnit.SECONDS.toMillis(1)); System.out.println("处理结束..."); System.out.println(); } @Override public void run() { synchronized (tasks) { // 这里的while和synchronized换换位置有什么差别吗? while (true) { try { while (tasks.size() == 0) { tasks.wait(); } process(tasks.poll()); tasks.notifyAll(); tasks.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } *************************************** public class Producer<T> implements Runnable{ private Queue<T> tasks; private int tasksMaxCount; private Scanner scanner; public Producer(Queue<T> tasks, int tasksMaxCount) { this.tasks = tasks; this.tasksMaxCount = tasksMaxCount; scanner = new Scanner(System.in); } private T produce() { System.out.println("请输入字符串:"); return (T)scanner.nextLine(); } @Override public void run() { synchronized (tasks) { while (true) { try { while (tasks.size() >= tasksMaxCount) { tasks.wait(); } tasks.add(produce()); tasks.notifyAll(); tasks.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } ****************************

作者回复: while放在里面其实就不会主动释放锁了。虽然wait的时候会释放锁,但是把process也包在sync里面了。这个就是浪费锁了。别的线程都在等这个线程process task,其实是没必要的。

2021-05-26
朱家华
回答代码中注释的问题: // 如果想给生产者消费做一个统计,统计每个生产者消费者所生产/消费的task数量,应该 // 1.使用哪种数据结构 // 2.如何保证线程安全 // 3.怎么将统计结果输出到控制台? 1.如果要给其统计生产和消费的数量,就需要找一个单独属于某个线程的变量。 线程栈是属于线程的,但线程栈里的栈帧执行完就弹出了,不长久。而堆中的数据所有线程都可以访问,又满足不了单独属于某个线程的变量。 2.如何保证线程安全: 目前已经学习的线程同步控制有synchronized,如果访问同一个变量的线程有分为生产和消费的关系,那么就在synchronized的基础上加入wait和notify。 3.将统计的数量输出到控制台: 由于新建的线程都继承创建线程的System.in,System.out,System.err。所以输出到控制的问题直接System.out.println()就好了。 老师,不知道回答的对不对

作者回复: 问题没有对错。能解决问题就是好的。如果两个人解决问题的思路相似,那么不稀奇。如果相同,那就肯定是抄的。 回到你的解决方案。问题1其实没有给方案,给了分析。其实堆里的数据也不都是共享的。你得先有对象的引用,才能操作这个对象。而且要操作的东西还得是public的才方便。 问题2的方案🉑️ 问题3的方案也🉑️

2021-05-26
2
笨笨
老师我运行了给的代码,但消费者有的出现了两次,例如消费者线程进入等待:消费者-98 这是正常的吗?

作者回复: 正常的,消费者线程会循环的去“抢”任务来做。只要不停止,消费者线程就会反反复复拿到任务,输出自己的线程名字

2021-01-17
笨笨
老师tasks.wait();是把队列里的所有线程都一块进入到wait状态吗?那如果再次执行tasks.wait.()会对之前就存在的线程且已经进入wait状态的线程有影响吗?

作者回复: wait方法是Object类的方法,线程一旦执行到这个方法,就会等待,直到别的线程调用这个对象的notify方法。 这里有线程,对象,两个角色。线程执行代码,对象提供数据(包括wait和notify这俩方法背后的锁),不同的线程可以操作相同的数据

2021-01-17
2
子非鱼
老师你好,有几个问题想要请教下 1.代码里面生产者和消费者都是获取 tasks 这一把锁,这样103个线程,同一时刻只能,有一个线程工作,导致的结果就是不能一边add消息,一边pull消息. 要是生产者一把锁,消费一把锁, 是不是就好一点 2.感觉生产者的 notifyAll() 会把生产者wait()等待的也唤醒吧 有点困惑,还请帮忙解答一下,三克油

作者回复: 1)锁是和task list对应的。保护的是不能有多个thread对这个list进行操作。否则就会有问题,不能一边执行add里的代码,一边执行remove里的代码,也不能同时执行add或者remove的代码,这些都会修改list的内部状态,同时多个线程去修改会有问题。 2)notifyAll是一个最佳实践,防止notify出现死锁。举个例子,因为notify是随便选择一个去唤醒的,那么就有可能是生产者的notify只唤醒了生产者,那么生产者就wait了,就释放锁了,但是如果在wait前没有notify消费者,那么就大家都在wait了,就死锁了。 简单来说,notify的行为可能会导致死锁,原因是它依赖被notify的线程执行的代码在wait前,有没有继续notify别的线程。

2020-08-06
2
春和景明
老师,Queue是个接口,里面定义的方法,为什么可以在这里直接使用?比如add poll这些,并没有默认实现呢

作者回复: queue虽然是个接口,但是它的引用指向的是实际实现了这个接口的对象。调用的方法也是实际对象所对应的类里的方法。

2020-03-25
收起评论