目录

Thread

前言

在java中,谈到线程,必然少不了Thread类。线程是比进程更轻量级的调度执行单位。为什么用线程?通过使用线程,可以把操作系统进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度(线程是CPU调度的基本单位)。

主流操作系统(Windows, Linux)都提供了线程的实现,Java则提供了在不同硬件和操作系统下对线程的统一处理,Thread类则是Java中线程的实现。

Java线程的实现方式

Java线程使用操作系统的内核线程实现,内核线程(Kernel-Level Thread, KLT)是直接由操作系统内核(Kernel,内核)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫做多线程内核(Muti-Threads Kernel)。

Java程序如何使用内核线程:

程序一般通过使用内核线程的高级接口—–轻量级进程(Light Weight Process, LWP),也就是我们通常意义上的线程。每个LWP都由一个内核线程支持。也就是说任何时候使用Java代码创建线程,调用Thread.start()的时候,都是通过LWP接口创建了KLT内核线程,然后通过OS的Thread Scheduler对内核线程进行调度分配CPU。线程模型如下图所示:

https://gitee.com/lienhui68/picStore/raw/master/null/20200731162040.png

内核线程的优点

每一个内核线程都是独立的轻量级进程,一个线程的阻塞不会影响整个进程的工作。

内核线程的缺点

  1. 由于是基于内核线程实现,各种线程的操作,如创建、析构、中断、休眠和同步,都需要系统调度(频繁从用户态切换进内核态),而系统调度的代价相对较高;

  2. 占用内核资源,同时轻量级进程的数量有限。

Java内存模型

Thread线程运行在Java Virtual Machine中,要理解Java中线程的运行方式,得先了解Java内存模型。Java虚拟机规范中定义了一种Java内存模型(Java Memory Model)来屏蔽各种硬件和操作系统的内存访问差异,以实现Java程序在各种平台下都能达到一致的内存访问效果(一次编译,随处运行得以实现的基础)。

主内存与工作内存:

JAVA内存模型规定了所有的变量都存储在主内存(Main Memory)中。所有的线程都有自己的工作内存,工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中执行,而不能直接读写主内存中的变量。同时,线程之间也无法读写各自的工作内存。关系图:

https://gitee.com/lienhui68/picStore/raw/master/null/20200731162151.png

线程状态转换

首先我们来看看操作系统中的线程状态转换。

在现在的操作系统中,线程是被视为轻量级进程的,所以操作系统线程的状态其实和操作系统进程的状态是一致的

https://gitee.com/lienhui68/picStore/raw/master/null/20200807142448.png

Java中的线程一共有六种状态,分别为New、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED,同一时刻只有一种状态,通过线程的getState方法可以获取线程的状态。

https://gitee.com/lienhui68/picStore/raw/master/null/20200807142518.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public enum State {
        /**
         * 当线程被创建出来还没有被调用start()时候的状态。
         * 反复调用同一个线程的start()方法是否可行?
         * 假如一个线程执行完毕(此时处于TERMINATED状态),再次调用这个线程的start()方法是否可行?
         * 两个问题的答案都是不可行,在调用一次start()之后,threadStatus的值会改变(threadStatus !=0),此时再次调用start()方法会抛出IllegalThreadStateException异常。比如,threadStatus为2代表当前线程状态为TERMINATED。
         */
        NEW,

        /**
         * 当线程被调用了start(),且处于等待操作系统分配资源(如CPU)、等待IO连接、正在运行状态,即表示Running状态和Ready状
         * 态。
         * 注:不一定被调用了start()立刻会改变状态,还有一些准备工作,这个时候的状态是不确定的。
         */
        RUNNABLE,

        /**
         * 等待监视锁,这个时候线程被操作系统挂起。当进入synchronized块/方法或者在调用wait()被唤醒/超时之后重新进入
         * synchronized块/方法,锁被其它线程占有,这个时候被操作系统挂起,状态为阻塞状态。
         * 阻塞状态的线程,即使调用interrupt()方法也不会改变其状态。
         */
        BLOCKED,

        /**
         * 无条件等待,当线程调用wait()/join()/LockSupport.park()不加超时时间的方法之后所处的状态,如果没有被唤醒或等待的
         * 线程没有结束,那么将一直等待,当前状态的线程不会被分配CPU资源和持有锁。
         * Object.wait():使当前线程处于等待状态直到另一个线程唤醒它;
         * Thread.join():等待线程执行完毕,底层调用的是Object实例的wait方法;
         * LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。
         */
        WAITING,

        /**
         * 有条件的等待,当线程调用sleep(睡眠时间)/wait(等待时间)/join(等待时间)/ LockSupport.parkNanos(等待时		
         * 间)/LockSupport.parkUntil(等待时间)方法之后所处的状态,在指定的时间没有被唤醒或者等待线程没有结束,会被系统自动
         * 唤醒,正常退出。
         */
        TIMED_WAITING,

        /**
         * 执行完了run()方法。其实这只是Java语言级别的一种状态,在操作系统内部可能已经注销了相应的线程,或者将它复用给其他需要
         * 使用线程的请求,而在Java语言级别只是通过Java代码看到的线程状态而已。
         */
        TERMINATED;
    }

时序图

https://gitee.com/lienhui68/picStore/raw/master/null/20200731162524.png

状态转换图

https://gitee.com/lienhui68/picStore/raw/master/null/20200731162735.png

https://gitee.com/lienhui68/picStore/raw/master/null/20200731162601.png

注意WAITING和BLOCKED状态:这两个状态下的线程都未运行,BLOCKED状态下,线程正等待锁;WAITING状态下,线程正等待被唤醒,唤醒后可能进入BLOCKED状态,继续等待锁,或者进入RUNNABLE状态,重新获取CPU时间片,继而等待获取锁。

线程方法

yield()

执行此方法会向系统线程调度器(Schelduler)发出一个暗示,告诉其当前JAVA线程打算放弃对CPU的使用,但该暗示,有可能被调度器忽略。使用该方法,可以防止线程对CPU的过度使用,提高系统性能。

sleep(time)

使当前线程进入休眠阶段,状态变为:TIME_WAITING

interrupt()

其作用是中断此线程(此线程不一定是当前线程,而是指调用该方法的Thread实例所代表的线程),但实际上只是给线程设置一个中断标志,线程仍会继续运行。允许当前线程对自身进行中断,否则将会校验调用方线程是否有对该线程的权限。

参考:https://blog.csdn.net/qq_39682377/article/details/81449451

如果当前线程因被调用Object#wait(),Object#wait(long, int), 或者线程本身的join(), join(long),sleep()处于阻塞状态中,此时调用interrupt方法会使抛出InterruptedException,而且线程的阻塞状态将会被清除。

interrupted()

返回true或者false, 查看当前线程是否处于中断状态,这个方法比较特殊之处在于,如果调用成功,会将当前线程的interrupt status清除。所以如果连续2次调用该方法,第二次将返回false。

isInterrupted()

回true或者false, 与上面方法相同的地方在于,该方法返回当前线程的中断状态。不同的地方在于,它不会清除当前线程的interrupt status状态。

join()

A线程调用B线程的join()方法,将会使A等待B执行,直到B线程终止。如果传入time参数,将会使A等待B执行time的时间,如果time时间到达,将会切换进A线程,继续执行A线程。

线程中断

线程中断

首先,我们要明白,中断不是类似 linux 里面的命令 kill -9 pid,不是说我们中断某个线程,这个线程就停止运行了。中断代表线程状态,每个线程都关联了一个中断状态,是一个 true 或 false 的 boolean 值,初始值为 false。

Java 中的中断和操作系统的中断还不一样,这里就按照状态来理解吧,不要和操作系统的中断联系在一起

关于中断状态,我们需要重点关注 Thread 类中的以下几个方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Thread 类中的实例方法,持有线程实例引用即可检测线程中断状态
public boolean isInterrupted() {}

// Thread 中的静态方法,检测调用这个方法的线程是否已经中断
// 注意:这个方法返回中断状态的同时,会将此线程的中断状态重置为 false
// 所以,如果我们连续调用两次这个方法的话,第二次的返回值肯定就是 false 了
public static boolean interrupted() {}

// Thread 类中的实例方法,用于设置一个线程的中断状态为 true
public void interrupt() {}

我们说中断一个线程,其实就是设置了线程的 interrupted status 为 true,至于说被中断的线程怎么处理这个状态,那是那个线程自己的事。如以下代码:

1
2
3
4
while (!Thread.interrupted()) {
   doWork();
   System.out.println("我做完一件事了,准备做下一件,如果没有其他线程中断我的话");
}

这种代码就是会响应中断的,它会在干活的时候先判断下中断状态,不过,除了 JDK 源码外,其他用中断的场景还是比较少的,毕竟 JDK 源码非常讲究。

当然,中断除了是线程状态外,还有其他含义,否则也不需要专门搞一个这个概念出来了。

如果线程处于以下三种情况,那么当线程被中断的时候,能自动感知到:

  1. 来自 Object 类的 wait()、wait(long)、wait(long, int),

    来自 Thread 类的 join()、join(long)、join(long, int)、sleep(long)、sleep(long, int)

    这几个方法的相同之处是,方法上都有: throws InterruptedException

    如果线程阻塞在这些方法上(我们知道,这些方法会让当前线程阻塞),这个时候如果其他线程对这个线程进行了中断,那么这个线程会从这些方法中立即返回,抛出 InterruptedException 异常,同时重置中断状态为 false。

  2. 实现了 InterruptibleChannel 接口的类中的一些 I/O 阻塞操作,如 DatagramChannel 中的 connect 方法和 receive 方法等

    如果线程阻塞在这里,中断线程会导致这些方法抛出 ClosedByInterruptException 并重置中断状态。

  3. Selector 中的 select 方法,参考下我写的 NIO 的文章

    一旦中断,方法立即返回

对于以上 3 种情况是最特殊的,因为他们能自动感知到中断(这里说自动,当然也是基于底层实现),并且在做出相应的操作后都会重置中断状态为 false

那是不是只有以上 3 种方法能自动感知到中断呢?不是的,如果线程阻塞在 LockSupport.park(Object obj) 方法,也叫挂起,这个时候的中断也会导致线程唤醒,但是唤醒后不会重置中断状态,所以唤醒后去检测中断状态将是 true。

InterruptedException 概述

它是一个特殊的异常,不是说 JVM 对其有特殊的处理,而是它的使用场景比较特殊。通常,我们可以看到,像 Object 中的 wait() 方法,ReentrantLock 中的 lockInterruptibly() 方法,Thread 中的 sleep() 方法等等,这些方法都带有 throws InterruptedException,我们通常称这些方法为阻塞方法(blocking method)。

阻塞方法一个很明显的特征是,它们需要花费比较长的时间(不是绝对的,只是说明时间不可控),还有它们的方法结束返回往往依赖于外部条件,如 wait 方法依赖于其他线程的 notify,lock 方法依赖于其他线程的 unlock等等。

当我们看到方法上带有 throws InterruptedException 时,我们就要知道,这个方法应该是阻塞方法,我们如果希望它能早点返回的话,我们往往可以通过中断来实现。

除了几个特殊类(如 Object,Thread等)外,感知中断并提前返回是通过轮询中断状态来实现的。我们自己需要写可中断的方法的时候,就是通过在合适的时机(通常在循环的开始处)去判断线程的中断状态,然后做相应的操作(通常是方法直接返回或者抛出异常)。当然,我们也要看到,如果我们一次循环花的时间比较长的话,那么就需要比较长的时间才能感知到线程中断了。

处理中断

一旦中断发生,我们接收到了这个信息,然后怎么去处理中断呢?本小节将简单分析这个问题。

我们经常会这么写代码:

1
2
3
4
5
6
try {
    Thread.sleep(10000);
} catch (InterruptedException e) {
    // ignore
}
// go on 

当 sleep 结束继续往下执行的时候,我们往往都不知道这块代码是真的 sleep 了 10 秒,还是只休眠了 1 秒就被中断了。这个代码的问题在于,我们将这个异常信息吞掉了。(对于 sleep 方法,我相信大部分情况下,我们都不在意是否是中断了,这里是举例)

AQS 的做法很值得我们借鉴,我们知道 ReentrantLock 有两种 lock 方法:

1
2
3
4
5
6
7
public void lock() {
    sync.lock();
}

public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}

前面我们提到过,lock() 方法不响应中断。如果 thread1 调用了 lock() 方法,过了很久还没抢到锁,这个时候 thread2 对其进行了中断,thread1 是不响应这个请求的,它会继续抢锁,当然它不会把“被中断”这个信息扔掉。我们可以看以下代码:

1
2
3
4
5
6
7
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 我们看到,这里也没做任何特殊处理,就是记录下来中断状态。
        // 这样,如果外层方法需要去检测的时候,至少我们没有把这个信息丢了
        selfInterrupt();// Thread.currentThread().interrupt();
}

而对于 lockInterruptibly() 方法,因为其方法上面有 throws InterruptedException ,这个信号告诉我们,如果我们要取消线程抢锁,直接中断这个线程即可,它会立即返回,抛出 InterruptedException 异常。

在并发包中,有非常多的这种处理中断的例子,提供两个方法,分别为响应中断和不响应中断,对于不响应中断的方法,记录中断而不是丢失这个信息。如 Condition 中的两个方法就是这样的:

1
2
void await() throws InterruptedException;
void awaitUninterruptibly();

通常,如果方法会抛出 InterruptedException 异常,往往方法体的第一句就是:

1
2
3
4
5
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
     ...... 
}

熟练使用中断,对于我们写出优雅的代码是有帮助的,也有助于我们分析别人的源码。

关于线程的几个小问题

sleep和yield的区别

  • sleep() 方法给其他线程运行机会时不考虑线程的优先级;yield() 方法只会给相同优先级或更高优先级的线程运行的机会
  • 线程执行 sleep() 方法后进入阻塞状态;线程执行 yield() 方法转入就绪状态,可能马上又得得到执行
  • sleep() 方法声明抛出 InterruptedException;yield() 方法没有声明抛出异常
  • sleep() 方法需要指定时间参数;yield() 方法出让 CPU 的执行权时间由 JVM 控制

sleep和wait的区别

  • sleep是线程中的方法,但是wait是Object中的方法。

  • sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。

  • sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。

  • sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。