zoukankan      html  css  js  c++  java
  • 《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

  • 相关阅读:
    加法原理和乘法原理
    布尔矩阵
    Codeforces Round #603 (Div. 2) A. Sweet Problem
    Codeforces Round #603 (Div. 2) D. Secret Passwords 并查集
    poj1611The Suspects并查集
    poj 2236 Wireless Network 并查集
    求斐波那契数的python语言实现---递归和迭代
    python语言实现阶乘的两种方法---递归和迭代
    栈实现二进制转十进制
    栈的基本操作
  • 原文地址:https://www.cnblogs.com/buwuliao/p/9538260.html
Copyright © 2011-2022 走看看