java多线程编程核心技术——第三章总结

第一节等待/通知机制


 

1.1不使用等待/通知机制实现线程间通信  

本小节中使用了sleep()与while(true)实现了多线程间的通信。

原理:有一公共类,内有List集合,并提供add()方法向其中添加元素。

线程A开启后每一秒钟调用add()方法

线程B开启后一直循环查询List的大小,当超过定值时抛出异常终止线程。

弊端:线程B不停的while语句轮询机制检查某一条件,浪费CPU。

若轮询时间小,CPU资源更加浪费,若轮询时间过大,则可能无法得到预想的值。


 

1.2什么是等待/通知机制


 

1.3等待/通知机制的实现

  方法wait()的作用是使当前执行代码的线程进行等待,wait()方法是Object类的方法,该方法用来将当前线程置入“预执行队列”中,并且在wait()所在的代码行处停止执行,直到接到通知或被中断为止。

在调用wait()之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步代码块中执行wait()方法。在执行wait()方法后,当前线程释放锁。在从wait()返回前,线程与其他线程竞争重新获得锁。若在调用wait()时没有持有适当的锁,则会抛出IllegalMonitorStateException,他是RuntimeException的一个子类,因此不需要try/catch进行捕捉异常。

  方法notify()也要在同步方法或同步代码块中调用,即在调用前,线程也必须获得该对象的对象级别锁。如果调用notify()时没有持有适当的锁,也会抛出IllegalMonitorStateException异常。该方法用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选出一个呈wait状态的线程,对其发出通知notify,并使它等待获取该对象的对象锁。需要说明的是,在执行notify()方法后,当前线程不会马上释放该对象锁,呈wait状态的线程也并不能马上获取该对象锁,要等到执行notify()方法的线程将程序执行完毕,也就是退出synchronized代码块后,当前线程才会释放锁,而呈wait状态所在的线程才能获取该对象锁。当第一个获得了该对象锁的wait线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用notify语句,则即便该对象以及空闲,其他wait状态等待的线程由于没有得到该对象的通知,还会继续阻塞在wait状态,直到这个对象发出一个notify或notifyAll。

  简而言之:

      • wait()使线程停止运行,notify()使停止的线程继续运行。
      • wait()与notify()都需要对象级别的锁存在才能调用。
      • wait()执行后,当前线程立即释放对象锁,notify()执行后,需等待同步代码执行完毕才能够释放对象锁,并唤醒其他线程。

  :wait()与notify()都是Object的方法,在“等待”跟“通知”时,均是由锁对象自己来调用wait()与notify()的。

  wait()方法可以使调用该方法的线程释放共享资源的锁,然后从运行状态退出,进入等待队列,知道被再次唤醒。

  notify()方法可以随机唤醒等待队列等待同一共享资源的“一个”线程,并使该线程退出等待队列,进入可运行状态,仅通知“一个”线程。

  notifyAll()可以使所有正在等待队列中等待统一资源的“全部”线程从等待状态退出,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于JVM实现。

 

  线程的几个状态:

 

  1)新创建一个线程对象后,再调用它的start()方法,系统会为此线程分配CPU资源,使其处于Runnable(可运行)状态,这是一个准备运行的阶段,如果线程抢占到CPU资源,此时线程处于Running(运行)状态。

  2)Runnable状态和Running状态可以互相切换,因为有可能线程运行一段时间后,有其他高优先级的线程抢占了CPU资源,这是线程就从Running状态转化为Runnable状态。

 

    线程进入Runnable状态大体分为以下5种情况:

      • 调用sleep()方法后经过的时间超过了指定的休眠时间。
      • 线程调用的阻塞IO已经返回,阻塞方法执行完毕。
      • 线程成功获得了试图同步的监视器。
      • 线程正在等待某个通知,其他线程发出了通知。
      • 处于挂起状态的线程调用了resume恢复方法。

  3)Blocked是阻塞的意思,例如遇到一个IO操作,此时CPU处于空闲状态,可能会转而把CPU时间片分给其他线程,这时也可以成为“暂停”状态。Blocked状态结束后,进入Runnable状态,等待系统重新分配资源。

    出现阻塞的情况大体分为以下5中:

      线程调用sleep()方法,主动放弃占用的处理器资源。

      线程调用阻塞式IO方法,在该方法返回前,该线程被阻塞。

      线程试图获得一个同步监视器,但该同步监视器正在被其他线程所持有。

      线程等待某个通知。

      线程调用suspend()方法将该线程挂起,此方法容易死锁,尽量避免使用。

  4)run()方法运行结束后进入销毁阶段,整个线程执行完毕。

   注:每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程。一个线程被唤醒才会进入就绪队列,等待CPU的调度,反之一个线程被wait后,就会进入阻塞队列,等待下一次唤醒。


 

1.4方法wait()锁释放与notify()锁不释放 

当方法wait()被执行完后,锁会被自动释放,但执行完notify()方法后,锁不自动释放。

  注:sleep()方法不释放锁,notify()方法执行后,必须方法所在的synchronized代码执行完毕后才释放锁。


 

1.5当interrupt方法遇到wait方法 

当线程呈wait()状态时,调用线程对象的interrupt()方法会出现InterruptedException异常。

注:

  1)执行完同步代码块就会释放对象的锁。

  2)在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放。

  3)在执行同步代码块的过程中,执行了锁所属对象的wait()方法,这个线程就会释放对象锁,而这个线程就会进入线程等待池中,等待被唤醒。


 

1.6只通知一个线程 

 调用notify()一次只随机唤醒一个线程。(前提是这两个线程使用同一个对象锁)。


 

1.7唤醒所有线程

调用notifyAll()方法会唤醒所有的线程 。(这些线程全都是使用一个对象锁)。


1.8方法wait(long)的使用

带一个参数的wait(long)方法的功能是等待某一时间内是否有线程对锁进行唤醒,如果超过这个时间就自动唤醒。

注:也可以在long的时间内被其他线程唤醒(这些线程公用一个对象锁)。


1.9通知过早

如果通知过早可能会打断程序正常的运行逻辑。

可能会出现,还没有wait,就已经notify的情况。这样就失去通知的意义了。


1.10等待wait的条件发生变化

若wait等待的条件发生了变化,也容易造成程序逻辑的混乱。

这个是结合到具体的逻辑判断与数据删减的情况才会涉及到。

这里是使用while()判断替换了if()判断解决的问题(但是目前我没想明白为啥。。)


 

1.11生产者/消费者模式实现

变形一:一生产与一消费——操作值:

  生产者:一个专门用来提供服务,并且在服务提供完成后唤醒消费者。

  消费者:专门用来消费服务,并且在消费完之后唤醒生产者。

变形二:多生产与多消费——操作值-假死:

  “假死”的现象就是线程进入了WAITING等待状态,如果全部线程都进入了WAITING状态(死锁),则程序就不再执行任何业务功能了,整个项目呈停止状态。

  案例中出现假死的现象是由于仅仅唤醒了同类(生产者唤醒了生产者,消费者唤醒了消费者)的现象大量出现导致的。

变形三:多生产与多消费——操作值:

  解决上一小节的假死问题的方式是,将notify()方法改为notifyAll()方法,这样就会通知所有的线程,避免出现假死状态。

变形四:一生产与一消费——操作栈:

  以List集合模拟栈,限制为栈的容量是否为1(最大值为1),进行生产/消费。

变形五:一生产与多消费——操作栈:解决wait条件改变与假死。

  同上例的基础条件,但是消费者变为多个。

  解决条件改变是将if改为while(这里还不太明白),解决假死是将notify()改为notifyAll()。

变形六:多生产与一消费:操作栈

变形七:多生产与多消费:操作栈

  六和七,在解决条件改变与假死之后,只需修改执行方法就ok,说明,生产者与消费者模型中目前出现的问你就是通过这两个方式来解决的。

  注:wait条件改变:while()替代if(),假死:notifyAll()替代notify()


 

1.12通过管道进行线程间通信:字节流

  java中有各种各样的输入/输出流Stream,使我们能够很方便的对数据进行操作,其中管道流(pipeStream)是一种特殊的流,用在不同管道间直接传送数据。

  一个线程发送数据到输出管道,一个线程从输入管道接收数据。通过不同线程间的管道通信,而无须借助于类似临时文件之类的东西。

  四种管道流:

    1)PipedInputStream/PipedOutputStream

    2)PipedReader/PipedWriter

  使用流的new PipedInputStream().connect(new PipedOutputStream)或者:new PipedOutputStream().connect(new PipedInputStream())。可以实现Stream之间的通信链接。

  注:当读取的数据没有值时,会阻塞,知道数据被写入之后才会继续运行。


 

1.13通过管道进行线程间通信:字符流  

  与上面的例子是一致的(流不一样)。


 

1.14实战:等待/通知之交叉备份

  要求:

    创建20个线程,十个线程将数据备份到数据库A,十个线程将数据备份到数据库B,且备份A与备份B是交叉进行的。同时还要求有序。

  数据库代码:

public class DBTools { volatile private boolean prevIsA = false; synchronized public void backupA() { try { while (prevIsA == true) { wait(); } for (int i = 0; i < 5; i++) { System.out.println("☆☆☆☆☆"); } prevIsA = true; notifyAll(); } catch (InterruptedException e) { e.printStackTrace(); } } synchronized public void backupB() { try { while (prevIsA == false) {    //用while代替if防止出现条件改变的情况 wait(); } for (int i = 0; i < 5; i++) { System.out.println("*****"); } prevIsA = false; notifyAll();            //用notifyAll()替换notify()防止出现“假死” } catch (InterruptedException e) { e.printStackTrace(); } } }

线程代码1:

public class BackupA extends Thread{ private DBTools db; public BackupA(DBTools db) { this.db = db; } @Override public void run() { db.backupA(); } }

线程代码2:

public class BackupB extends Thread { private DBTools db; public BackupB(DBTools db) { this.db = db; } @Override public void run() { db.backupB(); } }

执行代码:

public class Main { public static void main(String[] args) throws Exception{ DBTools dbTools = new DBTools(); for (int i = 0; i < 20; i++) { BackupB output = new BackupB(dbTools); output.start(); BackupA input = new BackupA(dbTools); input.start(); } } }

执行结果:


 

第二节方法join的使用

场景应用:主线程创建子线程,但是若主线程先于子线程终结,且想获得子线程中的值时,就需要join方法,作用:等待线程对象毁灭


 2.1学习方法join前的铺垫

 场景实例:


 

2.2用join()方法来解决

  只要在上例的代码start()后面加join()方法即可实现输出的要求。

  方法join的作用是使所属的线程对象x正常执行run()方法中的任务,而使当前线程z进行无限期的阻塞,等待线程x销毁后再继续执行线程z后面的代码。


 

2.3方法join与异常

 方法join()与interrupt()如果遇到就会出现异常。

  注:因调用join()而无限阻塞的线程必须与调用interrupt()的线程保持一致。


 

2.4方法join(long)的使用  

  join(long)中的参数是设定等待时间。

  join(long)与sleep(long)的区别:在同步上表现不一致。(调用者也不一样)


 

2.5方法join(long)与sleep(long)的区别

  join的源码中是通过调用wait(long)来实现的,具有释放锁的功能:

 public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }

  而Thread.sleep(long)却不释放锁。


 

2.6方法join()后面的代码提前运行:出现意外  

2.7方法join()后面的代码提前运行:解释意外

出现这样的情况在于join()方法也会去争抢锁对象,然后会释放锁对象。导致后面的代码会提前执行。

(实际上这一块我看的并不太明白,还会努力继续看,到时候会做补充笔记)

 


 

第三节类ThreadLocal的使用  

类ThreadLocal用来存储每个线程绑定自己的值。可以将其理解为一个全局存储数据的容器,且在容器中可以存储每个线程的私有数据。

 


 

3.1方法get()与NULL  

当创建一个ThreadLocal对象后,若直接调用get()方法会返回null。可以通过set()方法向其中添加值,再使用get()方法获取值。

  • get():从容器中获取值。
  • set():向容器中添加值。

 

3.2验证线程变量的隔离性 

  通过代码发现,当只有一个静态ThreadLocal变量时,多条线程使用set()向其中添加数据后,在使用get()获取数据,拿到的是线程各自添加到的数据,即多条线程的数据的插入与获取都没有相互干扰,线程间的数据是相互隔离的。

  注:发现问题,第一次调用get()方法时获取的值为null,如何实现第一次调用get()能返回默认值呢(下一节见)?

    对某一确定的线程来说,每一次set()都会覆盖前一次set()的值,get()获取的是最近一次set()操作插入的值。


3.3解决get()返回NULL问题

  继承ThreadLocal后,重写其中的initialValue()方法,并返回你所需要的默认值即可。此时,使用自定义的容器就可以实现get()有默认值。


3.4再次验证线程变量的隔离性

  上一小节中,通过继承然后重写initialValue()方法实现了默认值,那么设置默认值后,是否存在不同线程间存入的值相同的情况呢?

  实际上不存在,不同线程间存储的值还是不同的。

  案例中设置默认值为当前毫秒值,然后线程的启动之间有时间间隔,发现获取到的毫秒值是有区别的。所以隔离性是依然存在的。


 

第四节类InheritableThreadLocal的使用

使用该类InheritableThreadLocal可以在子线程中取得父线程继承下来的值。


4.1值继承

  使用自定义线程继承InheritableThreadLocal类可以使子线程从父线程中获取值。

  注:多线程中的父子线程指的是:一个线程开启另一个线程,开启线程的称为:父线程、被开启的线程称为:子线程。


4.2值继承再修改

在继承InheritableThreadLocal后,重写childValue(Object parentValue)就可以实现在子线程中对获取的值进行修改。


 

 

本文内容是书中内容兼具自己的个人看法所成。可能在个人看法上会有诸多问题(毕竟知识量有限,导致认知也有限),如果读者觉得有问题请大胆提出,我们可以相互交流、相互学习,欢迎你们的到来,心成意足,等待您的评价。