《JAVA多线程编程核心技术》 笔记:第三章:线程间通信

一、 等待/通知机制:wait()和notify()
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、一生产与一消费:操作值(不会假死)

根据值进行控制判断(什么时候进入阻塞状态)

总结:

  1. 首先:需要两个线程,生产者线程和消费者线程,生产者线程和消费者线程都必须一直执行。
  2. 其次,两个线程操作同一个对象。
  3. 生产者和消费者对该对象的操作有不同的逻辑:
    1. 生产者和消费者需要一个判断逻辑(该判断逻辑对生产者就是消费者处理后的状态,对消费者就是生产者生产后的状态),符合逻辑之后才能进入自己的wait;
    2. 生产者往该对象set值,set之后通知消费者;
    3. 消费者从该对象取值并消费处理,处理后通知生产者;
  4. 以上-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 线程状态转换:

新建之后 可运行状态(从运行状态变为可运行状态) 运行状态 阻塞状态 销毁状态
  1. 等待被分配资源
  1. 执行了sleep()方法,同时等待时间超过设定时间
  1. 执行了sleep()方法,同时等待时间未超过设定时间
  1. run方法运行结束之后
  1. 线程调用了阻塞式IO方法,且已经返回了结果,阻塞方法执行完毕;
  1. 线程调用了阻塞式IO方法,且该方法返回结果之前;
  1. 线程成功获取了试图同步的监视器
  1. 线程试图获取了一个同步监视器,但该同步的监视器被其他线程持有。
4.wait线程收到其他线程发出的notify通知
  1. 线程执行了wait()方法,等待某个通知。
  1. 被suspend方法挂起的线程调用了resume方法。
  1. 线程调用了suspend方法将该线程挂起。
  1. 调用start方法,系统为其分配资源(第一次进入可运行状态)

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之后,就会进入阻塞队列,等待下一次被唤醒)

END

原文地址:https://www.cnblogs.com/buwuliao/p/9538260.html