4.1 线程简介

4.1.1 什么是线程

现代操作系统在运行一个程序时,会为其创建一个进程。
现代操作系统的调度最小单位是线程,也叫轻量级进程,在一个进程中可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。

4.1.2 为什么要使用多线程

  1. 更多的处理器核心
  2. 更快的响应时间
  3. 更好的编程模型

4.1.3 线程优先级

现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待下次分配。线程分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。

JAVA线程中,通过一个整型变量;priority来控制优先级,优先级的范围为1-10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级为5,优先级高的线程分配时间片的数量要多于优先级低的线程。

针对频繁阻塞(休眠或I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。在不同的JVM以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。
**

4.1.4 线程的状态

状态 说明
NEW 初始状态,线程被构建,但是还没有调用start()方法
RUNNABLE 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”
BLOCKED 阻塞状态,表示线程阻塞于锁
WAITING 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
TIME_WAITING 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回
TERMINATED 终止状态,表示当前线程已经执行完毕

线程在自身生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换
image.png

4.1.5 Daemon线程

Daemon线程是一种支持线程,因为它主要被用作程序中后台调度以及支撑性工作。当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。

Daemon属性需要在启动线程之前设置,不能在启动线程之后设置 在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑

**

4.2 启动和终止线程

4.2.1 构造线程

在运行线程之前首先要构造一个线程对象,线程对象再构造的时候需要提供线程所需要的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息
Thread.java

  1. private void init(ThreadGroup g, Runnable target, String name,
  2. long stackSize, AccessControlContext acc,
  3. boolean inheritThreadLocals) {
  4. if (name == null) {
  5. throw new NullPointerException("name cannot be null");
  6. }
  7. this.name = name;
  8. // 当前线程就是该线程的父线程
  9. Thread parent = currentThread();
  10. SecurityManager security = System.getSecurityManager();
  11. if (g == null) {
  12. /* Determine if it's an applet or not */
  13. /* If there is a security manager, ask the security manager
  14. what to do. */
  15. if (security != null) {
  16. g = security.getThreadGroup();
  17. }
  18. /* If the security doesn't have a strong opinion of the matter
  19. use the parent thread group. */
  20. if (g == null) {
  21. g = parent.getThreadGroup();
  22. }
  23. }
  24. /* checkAccess regardless of whether or not threadgroup is
  25. explicitly passed in. */
  26. g.checkAccess();
  27. /*
  28. * Do we have the required permissions?
  29. */
  30. if (security != null) {
  31. if (isCCLOverridden(getClass())) {
  32. security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
  33. }
  34. }
  35. g.addUnstarted();
  36. this.group = g;
  37. // 将daemon、priority属性设置为父线程的对应属性
  38. this.daemon = parent.isDaemon();
  39. this.priority = parent.getPriority();
  40. if (security == null || isCCLOverridden(parent.getClass()))
  41. this.contextClassLoader = parent.getContextClassLoader();
  42. else
  43. this.contextClassLoader = parent.contextClassLoader;
  44. this.inheritedAccessControlContext =
  45. acc != null ? acc : AccessController.getContext();
  46. this.target = target;
  47. setPriority(priority);
  48. // 将父线程的InheritableThreadLocal复制过来
  49. if (inheritThreadLocals && parent.inheritableThreadLocals != null)
  50. this.inheritableThreadLocals =
  51. ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  52. /* Stash the specified stack size in case the VM cares */
  53. this.stackSize = stackSize;
  54. // 分配一个线程ID
  55. tid = nextThreadID();
  56. }

一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了parent是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时还会分配唯一的ID来标识这个child线程。

4.2.2 启动线程

线程对象在初始化完成之后,调用start()方法就可以启动这个线程。线程start()方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。

4.2.3 理解中断

中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt()方法对其进行中断操作。
线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。如果该线程已经处于终结状态,即使线程被中断过,在调用该线程对象的isInterrupted()依旧会返回false。
在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false。

4.2.4 安全地终止线程

main线程通过中断操作和cancel()方法均可使CountThread得到终止。这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程终止,因此这种终止线程的做法显得更加安全和优雅。

4.3 线程间通信

4.3.1 volatile和synchronized关键字

Java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝,所以程序在执行过程中,一个线程看到的变量并不一定是最新的。
关键字volatile可以用来修饰字段,就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排它性。
image.png
任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

4.3.2 等待/通知机制

image.png
等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通过后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
调用wait()、notify()以及notifyAll()时需要注意的细节:

  1. 使用wait()、notify()和notifyAll()时需要先对调用对象加锁
  2. 调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列
  3. notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifyAll()的线程释放锁之后,等待线程才有机会从wait()返回。
  4. notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED。
  5. 从wait()方法返回的前提是获得了调用对象的锁。

等待/通知机制依托于同步机制,其目的就是确保等待线程从wait()方法返回时能够感知到通知线程对变量做出的修改
image.png

4.3.3 等待/通知的经典范式

等待放遵循如下原则:

  1. 获取对象的锁
  2. 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件
  3. 条件满足则执行对应的逻辑

对应的伪代码如下:

  1. synchronized(对象){
  2. while(条件不满足){
  3. 对象.wait();
  4. }
  5. 对应的处理逻辑
  6. }

通知方遵循如下原则:

  1. 获得对象的锁
  2. 改变条件
  3. 通知所有等待在对象上的线程

对应的伪代码如下:

  1. synchronized(对象){
  2. 改变条件
  3. 对象.notifyAll();
  4. }

4.3.4 管道输入/输出流

管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存.
管道输入/输出流主要包括如下4种具体实现:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节,而后两种面向字符。
对于 Piped类型的流,必须先要进行绑定,也就是调用connect()方法,如果没有将输入/输出流绑定起来,对于该流的访问将会抛出异常。

4.3.5 Thread.join()的使用

如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。线程Thread除了提供join()方法之外,还提供join(long millis)和join(long millis, int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回。
Thread.java

  1. public final synchronized void join(long millis)
  2. throws InterruptedException {
  3. long base = System.currentTimeMillis();
  4. long now = 0;
  5. if (millis < 0) {
  6. throw new IllegalArgumentException("timeout value is negative");
  7. }
  8. if (millis == 0) {
  9. // 条件不满足,继续等待
  10. while (isAlive()) {
  11. wait(0);
  12. }
  13. // 条件符合,方法返回
  14. } else {
  15. while (isAlive()) {
  16. long delay = millis - now;
  17. if (delay <= 0) {
  18. break;
  19. }
  20. wait(delay);
  21. now = System.currentTimeMillis() - base;
  22. }
  23. }
  24. }

当线程终止时,会调用线程自身的notifyAll()方法,会通知所有等待在该线程对象上的线程。

4.3.6 ThreadLocal的使用

ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值.
可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。

4.4 线程应用实例

4.4.1 等待超时模式

调用一个方法时等待一段时间,如果该方法能够在给定的时间段之内得到结果,那么将结果立刻返回,反之,超时返回默认结果。
假设超时时间段是T,那么可以推断出在当前时间now+T之后就会超时。
定义如下变量:

  • 等待持续时间:REMAINING=T
  • 超时时间:FUTURE=now+T

这时超时仅仅需要wait(REMAINING)即可,在wait(REMAINING)返回之后会将执行:REMAINING=FUTUTRE-now。如果REMAINING小于等于0,表示已经超时,直接退出,否则将继续执行wait(REMAINING).

  1. public synchronized Object get(long mills) throws InterruptedException{
  2. long futuer = System.currentTimeMillis() + mills;
  3. long remaining = mills;
  4. //当超时大于0并且result返回值不满足要求
  5. while((result == null) && remaining > 0){
  6. wait(remaining);
  7. remaining = futuer - System.currentTimeMillis();
  8. }
  9. return result;
  10. }