《JAVA多线程编程核心技术》 笔记:第三章:线程间通信
1.1、使用的原因:
1.2 具体实现:wait()和notify()
1.2.1 方法wait():
1.2.2 方法notify():
1.2.3 wait()和notify()使用对比:
1.3 当interrupt方法遇到wait方法
1.4 notifyAll():唤醒所有线程
1.5 方法wait(long)
二、方法join的使用
2.1 join()方法的作用:本质是wait()
2.2 join和synchronized的区别:
2.3 join()的异常
2.4 方法join(long):(本质是wait(long))
2.5 join(long)和sleep(long)的区别
2.6 注意:方法 join(long)后面的代码提前运行会出现意外
三、通过管道进行线程间通信
3.1 字节流管道
3.2 字符流管道
3.3 实战:等待/通知值交叉备份
四、生产者/消费者模式实现:wait和notify
4.0、正常和异常说明
4.1、一生产与一消费:操作值(不会假死)
4.2、多生产和多消费:操作值(假死)
4.3、多生产和多消费:操作值(假死解决)
4.4、一生产与一消费:操作栈
4.5、一生产与多消费:操作栈(解决wait条件改变与假死:if和while循环的不同以及解决)
4.5.1 if 会出现异常的原因分析:
4.5.2 while可以解决if的异常,但会导致假死
判断条件if和while里执行wait()操作的区别:
4.5.3 假死的解决:notifyAll
4.6、多生产与一消费:操作栈
4.7、多生产与多消费:操作栈
五、类ThreadLocal的使用:
5.1 方法get()和null
5.2 ThreadLocal如何实现线程变量的隔离性
六、类InheritableThreadLocal的使用:
6.1值继承
6.2 值继承再修改
七、线程状态 + 方法+ 就绪和阻塞队列-总结
7.1 线程状态转换:
7.2 线程方法说明:
7.3 就绪队列和阻塞队列
END
一、 等待/通知机制:wait()和notify()
1.1、使用的原因:
如果没有通知等待机制,则只能让线程使用while(true)
死循环,来一直执行。不断的进行条件判断,等到符合条件便自动退出。但这样线程便一直执行(轮询),会浪费CPU资源。
由此,引入等待/通知机制(原理不过说明)。
1.2 具体实现:wait()和notify()
wait()使线程停止运行,notify()使停止的线程继续运行。
1.2.1 方法wait():
wait()方法:将当前线程置于“预执行队列”,并在wait()所在的代码行处停止执行,直到接到通知或者被中断为止。
使用注意:
调用
wait()
之前:线程必须获得该对象的对象级别锁(即只能在同步方法或同步代码块中调用wait()方法)调用
wait()
时:如没有持有适当的锁,则抛出IllegalMonitorStateException(RunTimeException的一个子类,无需try/catch)执行
wait()
之后:当前线程释放锁从
wait()
返回前(即调用notify()之后):线程与其他线程竞争重新获得锁。
1.2.2 方法notify():
方法notify():用来通知那些可能等待该对象的对象锁的其他线程。如有多个线程等待,则由线程规划器随机挑选一个呈wait状态的线程,对其发出通知notify,并使他等待获取该对象的对象锁。
使用注意:
调用notify()
之前:线程必须获得该对象的对象级别锁(即只能在同步方法或同步代码块中调用notify()
方法)
调用notify()
时:如没有持有适当的锁,则抛出IllegalMonitorStateException(RunTimeException的一个子类,无需try/catch)
执行notify()
之后:
- 当前线程不会马上释放该对象锁,呈wait状态的线程也不能马上获取该对象锁。
- 要等到执行notify()的线程将程序执行完,即退出synchronized代码块后,当前线程才会释放锁,而呈wait状态的线程才可以获取该对象锁(是可以获取,不是获取到)。
- 当第一个获得了该对象锁的wait线程运行完毕之后,它会释放掉该对象锁。此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,还会继续阻塞在wait()状态。直到这个对象发出一个notify或notifyAll。
notify一次只能通知一个线程,而每个wait的线程都只有被noyify之后才会执行。
1.2.3 wait()和notify()使用对比:
wait() | notify() |
---|---|
调用前 | 必须获得该对象的对象级别锁 |
调用时 | 没有持有适当的锁,则抛异常 |
执行后(等待被 notify()唤醒时)+锁释放 |
当前线程立马释放锁; 线程从运行状态退出,进入阻塞状态,进入等待队列直到被再次唤醒 |
被notify()唤醒后 | 线程进入就需状态,重新尝试获取对象锁,并执行wait后续代码 |
1.3 当interrupt方法遇到wait方法
当线程wait状态时,调用线程的interrupt()方法会出现InterruptedException异常。(该异常由wait方法抛出。其实遇到sleep方法和join方法同样抛异常)
更多理解可参考:阻塞(sleep等等)区别 中断(interrupt)+ 中断的意义 - baoendemao - 博客园 https://www.cnblogs.com/baoendemao/p/3804730.html
1.4 notifyAll():唤醒所有线程
1.5 方法wait(long)
wait(long)和sleep的原理很像,到期自动唤醒,相当于到期自动执行一个notify。未到期之前也可被其他notify唤醒。
二、方法join的使用
2.1 join()方法的作用:本质是wait()
方法定义:等待线程对象销毁。(即当线程销毁之后,执行的线程继续执行)
实例解释:是所属的线程对象x正常执行run()方法中的任务,而当前线程z进行无限期的阻塞,等待线程x销毁后再继续执行线程z后面的代码。
作用:可使线程排队运行的作用,有类似同步的运行效果(synchronized)。
2.2 join和synchronized的区别:
join | synchronized |
---|---|
区别 | 内部使用wait等待 |
2.3 join()的异常
如果一个线程z在等待另一个x的join,忽然线程z调用了interrupt,那么线程z会出现异常。
但线程x还在继续执行,因为线程x没有出现异常。
理解:join本质是wait,wait遇到interrupt会抛出异常。
2.4 方法join(long):(本质是wait(long))
join(long)的理解
说明:方法join(long)中的参数是设定等待的时间。(和sleep很像)
即使线程x需要执行很久,但是只要join(long)时间到了,线程z就会继续往下执行。
- 如果在long时间内,线程没有执行完,那么以long为准。(即使线程没有执行完,线程还是会继续执行,和当前线程无关了)
- 如果在不到long的时间内,线程就执行完了,那么就以实际时间为准。
2.5 join(long)和sleep(long)的区别
方法 join(long):内部使用wait(long)实现,所以其会释放(当前线程持有的)锁。
而sleep(long):并不会释放锁。
2.6 注意:方法 join(long)后面的代码提前运行会出现意外
这个例子说了一个问题:
- join(long)会抢到锁,然后立即释放,就是为了进入wait(long)的等待队列;
- 当long过去之后,join(long)的线程会被唤醒,继续抢锁,执行后续代码。
- 但是如果其他线程也在抢锁,那么谁会抢到就不确定了。
因为不确定,所以可能会有问题。
三、通过管道进行线程间通信
3.1 字节流管道
原理和List一样,不过对于管道输入流来说,其自带的read方法,如果读取不到数据,就会自己阻塞。无需像list那些需自行让线程wait
3.2 字符流管道
和上一个没有太多区别,只是这个是字符流,上一个是字节流。
3.3 实战:等待/通知值交叉备份
只是让两个线程交替执行而已,使用一个boolean变量作为开关进行控制,没太多需要说明。
四、生产者/消费者模式实现:wait和notify
4.0、正常和异常说明
正常模式:生产1个→消费1个→生产1个→消费1个→生产1个→消费1个;
消费异常模式:生产1个→消费1个→再消费一个(无法消费,自己阻塞。然后只能等待生产者生产后将自己唤醒)→.......→生产1个→消费1个→生产1个→消费1个;
生产异常模式:生产1个→消费1个→生产1个→再生产一个(无法生产,自己阻塞。然后只能等待消费者消费后将自己唤醒)→.......→消费1个→生产1个→消费1个;
注意:一直只有一个阻塞,所以无需担心notify被错误消费;
4.1、一生产与一消费:操作值(不会假死)
根据值进行控制判断(什么时候进入阻塞状态)
总结:
- 首先:需要两个线程,生产者线程和消费者线程,生产者线程和消费者线程都必须一直执行。
- 其次,两个线程操作同一个对象。
- 生产者和消费者对该对象的操作有不同的逻辑:
- 生产者和消费者需要一个判断逻辑(该判断逻辑对生产者就是消费者处理后的状态,对消费者就是生产者生产后的状态),符合逻辑之后才能进入自己的wait;
- 生产者往该对象set值,set之后通知消费者;
- 消费者从该对象取值并消费处理,处理后通知生产者;
- 以上-END!
4.2、多生产和多消费:操作值(假死)
假死实际不是很理解...不过知道了原因,是因为notify唤醒了不该唤醒的wait,导致notify被错误消费(消费之后应再有一个notify,错误消费之后就没有了),然后后续逻辑错误,因此假死。
4.3、多生产和多消费:操作值(假死解决)
解决上述假死:将notify换为notifyAll
4.4、一生产与一消费:操作栈
根据list的size进行控制判断(什么时候进入阻塞状态)
4.5、一生产与多消费:操作栈(解决wait条件改变与假死:if和while循环的不同以及解决)
4.5.1 if 会出现异常的原因分析:
多个消费者,都处于阻塞;
如果一个消费者消费之后,执行notify(notify是随机唤醒),而该notify被另一消费者使用,另一消费者直接往下执行(不进行while的额外一重判断),直接执行后面,导致异常。
4.5.2 while可以解决if的异常,但会导致假死
while可以解决,因为while和if不一样。while那么肯定会再一次判断,判断发现是0,然后自己阻塞(即notify被浪费)了,然后导致了假死....
判断条件if和while里执行wait()操作的区别:
当被notify时:
- 如果是if,那么直接往下执行;
- 如果是while,那么会把判断条件再执行一次,这是由while本身语法决定的。
- 执行之后,再次满足才会往下执行;
- 如果不满足,则再次wait阻塞;
4.5.3 假死的解决:notifyAll
notifyAll肯定会唤醒生产者,生产者肯定会生产一个,然后继续消费,一直循环下去,肯定不会阻塞。
4.6、多生产与一消费:操作栈
这个好像没什么问题
4.7、多生产与多消费:操作栈
这个好像也没什么问题
五、类ThreadLocal的使用:
所有线程共享同一个变量情况:public static
每个线程都有自己的共享变量:使用ThreadLocal(主要解决:每个线程绑定自己的值,可以将其理解为全局存放数据的盒子,盒子中可以存放每个线程的私有数据)
5.1 方法get()和null
get()第一次调用会返回null(看源码:因为ThreadLocal的initialValue()方法返回的就是null,即每次初始化为null),除非进行set()的操作
5.2 ThreadLocal如何实现线程变量的隔离性
ThreadLocal(public static)只有一个,但每个线程只可以放入自己的值,取值的时候只会取出来自己的值,这个好像是代码自己实现的。我操,这才是ThreadLocal的牛掰之处。
为什么会这样?可以看下ThreadLocal的get和set方法。里面每次都会获取当前线程,然后再进行后续逻辑。内部是一个 ThreadLocalMap。
六、类InheritableThreadLocal的使用:
6.1值继承
使用InheritableThreadLocal可以在子线程中取得(子线程中取的是父线程的值,自己没有相关值)父线程继承下来的值。
6.2 值继承再修改
子线程可以覆盖父线程的childValue()方法,对主线程的值进行额外处理。
注意:如果子线程取值时,主线程将InheritableThreadLocal中的值进行了修改,那么子线程取到的还是旧值。
七、线程状态 + 方法+ 就绪和阻塞队列-总结
7.1 线程状态转换:
新建之后 | 可运行状态(从运行状态变为可运行状态) | 运行状态 | 阻塞状态 | 销毁状态 |
---|---|---|---|---|
|
|
|
|
|
|
|
|||
|
|
|||
4.wait线程收到其他线程发出的notify通知 |
|
|||
|
|
|||
|
7.2 线程方法说明:
start() 和run() | start(): 线程准备执行,具体执行由线程调度器决定 |
yield()和sleep() | yield(): 停止当前正在执行的线程,释放当前锁,让同样优先级的正在等待的线程有机会执行(只是有机会,具体怎么执行看系统,也可能还是自己执行) |
suspend()和resume() | suspend(): 使当前线程阻塞,不释放对象锁,只能被resume()恢复。 |
wait()和notify()和notifyAll() | wait(): 释放当前锁,等待被notify()通知 |
stop() | 停止线程,强制停止,不安全。 |
interrupt()和interrupted()和isInterrupted() | 中断线程。 调用该方法的线程的状态为将被置为"中断"状态。 并非真的立即停止。 更深的理解参考:阻塞(sleep等等)区别 中断(interrupt)+ 中断的意义 - baoendemao - 博客园 https://www.cnblogs.com/baoendemao/p/3804730.html |
7.3 就绪队列和阻塞队列
每个锁对象都有两个对列,就绪队列和阻塞队列。
就绪队列:存储将要获得锁的线程。(线程被唤醒后才会进入就需队列,等待CPU的调度)
阻塞队列:存储了被阻塞的线程。(线程被wait之后,就会进入阻塞队列,等待下一次被唤醒)