前言
在进入多线程和并发代码学习前,我们得先了解线程这玩意儿。通常需要了解的内容有:
- 线程有哪几种状态? 分别说明从一种状态到另一种状态转变有哪些方式?
- 通常线程有哪几种使用方式?
- 基础线程机制有哪些?
- 线程之间有哪些协作方式?
- 线程的中断机制?
首先讲一个老生常谈的概念,进程与线程:
- 进程 :指在系统中正在运行的一个应用程序;程序一旦运行就是进程; 进程是资源分配的最小单位。
- 线程 :系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。 线程是程序执行的最小单位。
线程是一个操作系统级别的概念。 JAVA语言(包括其他编程语言)本身不创建线程;而是调用操作系统层提供的接口创建、控制、销毁线程实例。
比如 Thread
的方法:
private native void start0();
最底层方法用的是 native
方法,调用的是 C、C++
操作系统本地方法。
开始之前老规矩,把源码放上:
- Thread 源码解读注释:https://gitee.com/qianwei4712/JDK1.8.0.25-read/blob/master/src/main/java/java/lang/Thread.java
- Object 源码解读注释:https://gitee.com/qianwei4712/JDK1.8.0.25-read/blob/master/src/main/java/java/lang/Object.java
线程的状态及状态转换
操作系统层面,线程有以下状态:
这五种状态是最基本的转换模型。在不同的编程语言中,会有细微区别。
对 Java
来说,线程状态已经确定在了 Thread 内部类的枚举中:
public enum State {
NEW,//尚未启动的线程的线程状态
RUNNABLE,//可运行线程的线程状态。处于可运行状态的线程正在Java虚拟机中执行,但可能正在等待来自操作系统的其他资源,例如处理器。
BLOCKED,//阻塞状态
WAITING,//等待线程的线程状态
TIMED_WAITING,//具有指定等待时间的等待线程的线程状态
TERMINATED;//终止线程的线程状态。线程已完成执行。
}
Blocking、 Waiting、Timed Waiting 其实都是休眠状态,在 Java 中进行了区分。
可运行状态和运行状态合并成 Runnable。
然后看看详细介绍
- New(新建)
线程对象被创建时,它只会短暂地处于这种状态。此时它已经分配了必须的系统资源,并执行了初始化。
相当于,这个线程还没有调用 start()
方法。
- Runnable(可运行/就绪/运行中)
Runnable
状态包括了操作系统线程状态中的 Running 和 Ready,也就是处于此状态的线程可能正在运行,也可能正在等待系统资源,如等待 CPU 为它分配时间片,如等待网络IO读取数据。
线程调度程序会从可运行线程池中选择一个线程作为当前线程。这也是线程进入运行状态的唯一一种方式。
所以线程只能从,可运行状态进入运行中状态。
- 调用线程的
start()
方法,此线程进入就绪状态。 - 当前线程时间片用完了,调用当前线程的
yield()
方法,当前线程进入就绪状态。 - 锁池里的线程拿到对象锁后,进入就绪状态。
- 当前线程
sleep()
方法结束,其他线程join()
结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
- Blocking(阻塞)
Blocking 称为阻塞状态,或者说线程已经被挂起,原因通常是它在等待一个“锁”,当尝试进入一个 synchronized 语句块/方法时,锁已经被其它线程占有,就会被阻塞,直到另一个线程走完临界区或发生了相应锁对象的 wait() 操作后,它才有机会去争夺进入临界区的权利。
在 Java 代码中,需要考虑 synchronized 的粒度问题,否则一个线程长时间占用锁,其它争抢锁的线程会一直阻塞,直到拥有锁的线程释放锁。
处于 BLOCKED 状态的线程,即使对其调用 thread.interrupt() 也无法改变其阻塞状态,因为 interrupt() 方法只是设置线程的中断状态,即做一个标记,不能唤醒处于阻塞状态的线程。
- Waiting(无限期等待)
等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
进入方法 | 退出方法 |
---|---|
没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() |
没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 |
LockSupport.park() 方法 | - |
- Timed Waiting(限期等待)
无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
调用 Thread.sleep()
方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。
调用 Object.wait()
方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。
睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。
阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep()
和 Object.wait()
等方法进入。
进入方法 | 退出方法 |
---|---|
Thread.sleep() 方法 | 时间结束 |
设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() / Object.notifyAll() |
设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 |
LockSupport.parkNanos() 方法 | - |
LockSupport.parkUntil() 方法 | - |
- Terminated(死亡)
可以是线程结束任务之后自己结束,或者产生了异常而结束。
其实这只是 Java 语言级别的一种状态,在操作系统内部可能已经注销了相应的线程,或者将它复用给其他需要使用线程的请求,而在 Java 语言级别只是通过 Java 代码看到的线程状态而已。
“阻塞”与“等待”的区别:
(1)“阻塞”状态是等待着获取到一个排他锁,进入“阻塞”状态都是被动的,离开“阻塞”状态是因为其它线程释放了锁,不阻塞了;
(2)“等待”状态是在等待一段时间 或者 唤醒动作的发生,进入“等待”状态是主动的;
如主动调用 Object.wait() ,如无法获取到 ReentraantLock ,主动调用 LockSupport.park() ,如主线程主动调用 subThread.join() ,让主线程等待子线程执行完毕再执行。
离开“等待”状态是因为其它线程发生了唤醒动作或者到达了等待时间。
线程的使用方式
Java 创建线程的方式有:
- 实现 Runnable 接口
- 实现 Callable 接口
- 继承 Thread 类
实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。
Runnable 接口
Runnable 接口就一个方法:
/**
* Runnable接口应由任何类实现,其实例将由线程执行。 该类必须定义一个无参数的方法,称为run 。
* @since JDK1.0
*/
@FunctionalInterface
public interface Runnable {
/**
* 当实现接口的对象Runnable被用来创建一个线程,启动线程使对象的run在独立执行的线程中调用的方法。
* <p>方法run的一般合同是它可以采取任何行动。
*/
public abstract void run();
}
基本的使用方式为:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Runnable");
}
}).start();
Lamada 表达式则更为简编:
new Thread(() -> System.out.println("Runnable")).start();
Callable 接口
Callable 接口和 Runnable 相比,就多了一个返回值,使用方式有点差异。
实现方式为 call()
方法,带有返回值,可以抛出异常。
/**
* 返回结果并可能引发异常的任务。 实现者定义一个没有参数的单一方法,称为 call 。
* Callable 接口类似于 Runnable ,因为它们都是为其实例可能由另一个线程执行的类设计的。
* 然而,A Runnable 不返回结果,也不能抛出被检查的异常。
* 该 Executors 类包含的实用方法,从其他普通形式转换为 Callable 类。
* @since 1.5
* @param <V> the result type of method {@code call}
*/
@FunctionalInterface
public interface Callable<V> {
/**
* 计算一个返回值,如果不能计算那么抛出异常
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
Callable 不能直接作为 Thread 参数创建线程,而是用 Future
来装载返回结果。
FutureTask<String> ft = new FutureTask<>(new Callable<String>() {
@Override
public String call() throws Exception {
return "结束";
}
});
new Thread(ft).start();
System.out.println(ft.get());
Thread 类
最后就是最基本的继承 Thread 类了。
public class MyThread extends Thread {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
同样也是需要实现 run()
方法,因为 Thread 类也实现了 Runable
接口。
当调用 start()
方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run()
方法。
在实际开发中, 推荐使用接口实现方式使用线程 :
- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
- 继承整个 Thread 类开销过大,使用接口方式更加轻量。
线程基础机制
线程的机制,其实说的就是 Thread 的底层方法,所以这一部分将会介绍 Java Thread 的部分源码。主要包括:
priority 优先级、daemon 守护线程、sleep() 线程休眠、yield() 状态声明、interrupt 线程中断、其他 native 方法。
这一部分介绍的都是 JDK8
中的 Thread
类的一些 native
方法和最基础的机制。
native
方法调用操作系统本地方法,也可以把它看做 Java 中最底层部分。
虽然不知道这些在啥时候能用到,但是看起来还是很高大上的........
priority 优先级
在 Java doc
中有这样关于优先级的介绍:
每一条线程都有优先级。拥有更高优先级的线程在执行时优先于低优先级的线程。
当线程中执行的代码创建了一个新的线程对象,那么这个新的线程对象的优先级和创建它的线程的优先级相同。
- 规则性 : 优先级高的线程获取 CPU 的资源概率比较高 ;线程最终是由操作系统来分配 CPU 资源的,Java 只能为这条线程设置较高优先级,使其更有可能尽早获得运行。
- 随机性 : 在操作系统层面,就算线程设置了更高的优先级,也无法绝对保证优先执行,只是拥有更大的概率获得资源。
- 继承性 : 当线程中执行的代码创建了一个新的线程对象,那么这个新的线程对象的优先级和创建它的线程的优先级相同。 这个好理解,而且代码里也非常明确。
首先看看 Thread
关于优先级的参数:
// 线程优先级,int类型,范围为1-10,默认为5
private int priority;
// 线程可以具有的最低优先级。
public final static int MIN_PRIORITY = 1;
// 分配给线程的默认优先级
public final static int NORM_PRIORITY = 5;
// 线程可以具有的最高优先级。
public final static int MAX_PRIORITY = 10;
在一条线程初始化时,请删去其余代码,可以发现线程的 继承性
,如下:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
...
Thread parent = currentThread();
...
this.priority = parent.getPriority();
...
}
省略的其他多余代码,初始化线程,将启动线程的优先级传递到新线程。
然后,我们还能通过调用线程的 setPriority()
方法设置线程的优先级( 优先级的设定最好在 start() 之前 ):
public final void setPriority(int newPriority) {
ThreadGroup g;
//判定当前运行的线程是否有权修改该线程
checkAccess();
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
throw new IllegalArgumentException();
}
//获取该线程所属的线程组
if((g = getThreadGroup()) != null) {
// 判断是否高于此线程组的最高优先级
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
// 调用 native 方法设置优先级
setPriority0(priority = newPriority);
}
}
最后实际设置优先级的方法为:
private native void setPriority0(int newPriority);// 设置优先级
调用操作系统本地方法,所以 Java 只能通过操作系统开放的方法来管理线程。
daemon 守护线程
在 Java
中有两类线程:用户线程 (User Thread) 和 守护线程 (Daemon Thread)
在 JDK8 的 Thread 源码的官方注释中,关于守护线程有以下几句描述:
每一条线程都可能被标注为守护线程。当创建它的线程是一个守护线程时,新线程才是守护线程。
当 Java 虚拟机启动时,通常已经存在了一个非守护线程(这个线程通常会调用某指定类的名为main的方法)。
先说简单的,与线程优先级相同,守护线程的 继承性
也在线程创建 init
方法中:
this.daemon = parent.isDaemon();
//判断就是直接返回属性值
public final boolean isDaemon() {
return daemon;
}
这个没啥好解释的了。。。。然后继续看官方注释,可以发现:
- main 方法启动的线程是非守护线程(也就是用户线程)。
- 发生上述的两种情况时,Java 虚拟机将会退出。翻译一下其实就是,所有非守护线程结束时,Java 虚拟机才会停止。所以, Java 虚拟机中必定包含守护线程。
实际上,Java GC 就是守护线程。
是不是绕的脑阔疼。。。。
守护线程是服务线程,准确地来说就是服务其他的线程,具有”陪伴“特性。若进程中没有用户线程,则守护线程自动销毁。
给个小栗子提供下测试:
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
for (int i = 0; i < 10000; i++) {
Thread.sleep(1000);
System.out.println("守护线程尚未停止");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.setDaemon(true);
thread.start();
//主线程等待3秒
Thread.sleep(3000);
System.out.println("主线程(非守护线程)结束。。。守护线程即将关闭");
}
测试结果很明显了,就不贴了。。。。。。
还有就是:
设置守护线程必须在线程开始执行之前:
/**
* 将此线程标记为{@linkplain #isDaemon 守护线程}或用户线程。
* 当仅运行的线程都是守护程序线程时,Java虚拟机将退出。
* <p> 必须在线程启动之前调用此方法
* @param on 如果{@code true},则将该线程标记为守护线程
* @throws IllegalThreadStateException 如果此线程是 {@linkplain #isAlive alive}
* @throws SecurityException 如果 {@link #checkAccess} 确定当前线程无法修改此线程
*/
public final void setDaemon(boolean on) {
checkAccess();
if (isAlive()) {
throw new IllegalThreadStateException();
}
daemon = on;
}
/**
* @return 测试此线程是否仍然存在。如果一个线程已经启动并且尚未死亡,则该线程是活动的。
*/
public final native boolean isAlive();
看,这里用到了个 native 方法,最终决定线程的并不是 Java。
到这里忽然想起以前在 main 方法测试线程池的时候,主线程运行结束,虚拟机没有停止。
当时没有在意,现在想想应该是非守护线程没有全部关闭。
现在来研究下,太具体的不讲了,主要确定下是因为非守护线程没有关闭这件事:
- 比如已经一个线程池
ThreadPoolExecutor
的实例对象,我现在pool.execute()
提交了一个任务。 - 线程池内部有个
Worker
内部类,专门负责处理任务。执行任务的方法里写了个死循环(为啥要死循环我就不知道了,以后再研究)
final void runWorker(Worker w) {
......
//这里写了个死循环,会看这个 Worker有没有任务,如果没有就会去取
while (task != null || (task = getTask()) != null) {
......
}
- 最后就是这个
Worker
创建的时候并没有设置为守护线程,所以根据线程守护属性的继承性,它是用户线程。
又解决了以前的一个疑问,舒服。。。。。。抬走,下一个 ~~
sleep() 线程休眠
sleep(long millis)
方法就算是没学过并发也肯定见过。在刚开始学习的时候肯定见过这个方法的使用。
而且,不是还有段子,二期性能优化全靠这段代码嘛。。。
/**
* 使当前正在执行的线程进入休眠状态(暂时停止执行),以毫秒为单位,取决于系统定时器和调度程序的精度和准确性。
* 并且线程不会丢失监视器锁。
* @param millis 睡眠时间(以毫秒为单位)
* @throws IllegalArgumentException 如果{@code millis}的值为负
* @throws InterruptedException 如果有任何线程中断了当前线程。抛出此异常时,将清除当前线程的中断状态。
*/
public static native void sleep(long millis) throws InterruptedException;
这里有两个异常,虽然不知道 native 是如何抛出异常的,但是了解下,有这两异常。
这是一个静态方法,作用于当前使用这个方法的线程。
Thread 中还有一个更的睡眠方法:
/**
* @param millis 睡眠时间(以毫秒为单位)
* @param nanos {@code 0-999999} 额外的纳秒睡眠
* @throws IllegalArgumentException 如果{@code millis}的值为负,或者{@code nanos}的值不在{@code 0-999999}范围内
* @throws InterruptedException 如果有任何线程中断了当前线程。抛出此异常时,将清除当前线程的中断状态。
*/
public static void sleep(long millis, int nanos) throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
sleep(millis);
}
仔细一读简直瞎了我的狗眼。。。。。。意思就是,超过 500 微秒,算 1 毫秒????源码还能这么随意???
不过这里其实也能发现一个知识点:
操作系统对线程的管理只能精确到毫秒级别。
然后这里补充两个有点意思冷门知识:
Thread.currentThread().sleep()
和Thread.sleep()
的区别Thread.sleep(0)
有什么作用
先看第一个, Thread.currentThread().sleep()
和 Thread.sleep()
的区别。我就直接列了:
- 从上面 sleep 方法的代码以及注释中可以发现,它是作用于当前线程。
- 因为它是一个静态方法,所有线程共用一个方法。所以当当前线程调用这个方法时,并没有创建新的 Thread 对象。
Thread.currentThread().sleep()
则是先返回当前正在执行的线程的引用。这也是一个native
方法。
/**
* @return 返回对当前正在执行的线程对象的引用。
*/
public static native Thread currentThread();
- 所以实际上,两者并没有区别。只是一个是直接使用静态方法,一个是实例调用静态方法。
下面看第二个,Thread.sleep(0)
有什么作用直接引用大佬们的博客:
Thread.Sleep(0) 并非是真的要线程挂起0毫秒,意义在于这次调用Thread.Sleep(0)的当前线程确实的被冻结了一下,让其他线程有机会优先执行。
Thread.Sleep(0) 是你的线程暂时放弃cpu,让线程马上重新回到就绪队列而非等待队列,也就是释放一些未用的时间片给其他线程或进程使用,就相当于一个让位动作。
yield() 状态声明
Thread 还提供了一个方法,这个方法的作用是建议 CPU 处理线程。看如下代码和注释:
/**
* 向处理器提出建议,当前线程愿意让出CPU给其他线程。处理器也可以忽略此提示。
* <p>Yield 是一种启发式尝试,旨在提高线程之间的相对进展,否则将过度利用CPU。
* 应将其使用与详细的性能分析和基准测试结合起来,以确保它实际上具有所需的效果。
* <p>这是一个很少使用的方法。它可能在调试或者测试的时候,或者设计并发控制程序的时候很有用。
*/
public static native void yield();
确实在 Thread 源码内部没有这个方法的直接调用,JDK 内部也很少使用这个方法。
不过和上面的 Thread.sleep(0)
进行对比:
yield()
只是建议,CPU 并不一定采纳执行。就算 CPU 采纳,线程也依然是 RUNNABLE 状态。只是从运行状态变化到就绪状态。- 调用
Thread.sleep()
时,则会立即从 RUNNABLE 转化为 TIMED_WAITING 状态。
其他 native 方法
Thread 一些过时的 native 方法{ suspend0()挂起线程
,resume0()恢复挂起的线程
, stop0()停止线程
}就不介绍了。
上面已经介绍了绝大部分 native 方法,剩下的几个在这里统一做下简要介绍。
还有非常重要的中断机制,这个将会后面单独介绍。中断是线程中非常重要的内容,能写的很多。。。
- 设置线程名
Thread 中对操作系统进行线程名操作是这个方法:
private native void setNativeName(String name);// 设置线程名
但是在 Thread 类中,也有自己维护的线程名。
首先 Thread 维护了一个 char[]
作为线程名。
private volatile char name[]; //线程名
同时提供了 get/set
方法,对于其中的 set
方法:
/**
* 更改线程名
* @param name 此线程的新名称。
*/
public final synchronized void setName(String name) {
checkAccess();
this.name = name.toCharArray();
// 如果线程状态不为0(初始状态),说明线程已经启动
// 那就需要调用 native 方法进行更改。
if (threadStatus != 0) {
setNativeName(name);
}
}
如果线程已经启动,那么需要调用本地方法进行更改。。
那么如果没启动呢,就不需要调用了?。。。。那这样是不是说明:
应该对吧,逻辑通畅。
- 持有锁
锁的内容也是后面讲,瞟一眼这个代码注释:
/**
* 如果当前线程持有指定锁,则返回true
* <p>此方法旨在允许程序断言当前线程已持有指定的锁:
* <pre>
* assert Thread.holdsLock(obj);
* </pre>
* @param obj 测试锁所有权的对象
* @throws NullPointerException 如果obj为 null
* @return 如果当前线程持有指定锁,则返回true
* @since 1.4
*/
public static native boolean holdsLock(Object obj);
- 还有几个不知道什么作用的方法
private native static Thread[] getThreads();
private native static StackTraceElement[][] dumpThreads(Thread[] threads);
这两静态方法在 Thread 类没用到,好像和 JVM 栈有关。
既然没用到,那就不管了。。。
线程之间的协作方式
在一般的SSM框架增删改查中,我们很少需要用到多线程协作。(应该是基本用不到)
但是在稍微偏向技术方面,甚至是,自己做点小玩具,想要更加高效时,就需要用到多线程之间的协调通信。
刚开始学习并发,也不知道全不全。。。我目前找到的就这几种。
wait、notify 机制
首先要介绍的就是,并发中比较特殊的方法。
wait
和 notify
系列方法是写在 Object
中的, Object
在 JAVA 中的地位,那简直是老祖宗了,除了 Class
外,根本没有其他类有这种地位。。。
所以首先我提出的疑问就是,为什么要把这几个方法写在 Object 中,而不是 Thread 中?
在网上查了半天资料,才发现这居然还是一道号称艰难的面试题。
结论可能会涉及后面的知识,还有一些我都看不懂的,先全部列上:
- 首先是,wait 和 notify 都必须在同步中才能生效,这些方法都必须标识同步所属的锁。任意对象都可以作为锁 ,所以将这两方法设计在了 Object 类而不是 Thread 类中。
- wait 方法暂停的是持有锁的对象,所以想调用方式为
Object.wait()
,notify 也一样。 - wait 和 notify 不仅仅是普通方法或同步工具,更重要的是它们是 Java 中两个线程之间的通信机制。 如果不能通过 Java 关键字(例如 synchronized)实现通信此机制,同时又要确保这个机制对每个对象可用,那么 Object 类则是的合理的声明位置。
- 在 Java 中,为了进入代码的临界区,线程需要锁定并等待锁,他们不知道哪些线程持有锁,而只是知道锁被某个线程持有, 并且需要等待以取得锁, 而不是去了解哪个线程在同步块内,并请求它们释放锁。
说实话,大佬的理解我确实看不懂。。。。
同步 和 等待通知 是两个不同的领域,不要把它们看成是相同的或相关的。
同步是提供互斥并确保 Java 类的线程安全,而 wait 和 notify 是两个线程之间的通信机制 。
好嘞,然后继续。wait() 的三个重载方法中,两个方法调用另一个 native 本地方法。
//无参方法,默认 wait(0),无限期等待
public final void wait() throws InterruptedException {
wait(0);
}
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException("nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && timeout == 0)) {
timeout++;
}
wait(timeout);
}
最终产生作用的是下面这个本地方法:
/**
* 导致当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法,或指定的时间已过。
* <p>当前线程必须持有本身对象监视器。
* <p>此方法使当前线程(称为T)将自身置入等待 set 集合中,然后放弃该对象的所有同步声明。
*之后线程 T 无法成为线程调度的目标,并且休眠,直到发生四件事情之一:
* <ul>
* <li>一些其他线程调用该对象的notify方法,并且线程T恰好被任意选择为被唤醒的线程。
* <li>某些其他线程调用此对象的notifyAll方法。
* <li>一些其他线程interrupts线程T。
* <li>指定的实时数量已经过去,或多或少。 然而,如果timeout为零,则不考虑实时,线程等待直到通知。
* </ul>
* 然后从该对象的等待set集合中删除线程T ,并重新启用线程调度。
* 然后它以通常的方式与其他线程竞争在对象上进行同步的权限;
* 一旦获得了对象的控制,其对对象的所有同步声明就恢复到现状,也就是在调用wait方法之后的情况。
* 线程T然后从调用wait方法返回。 因此,从返回wait方法,对象和线程的同步状态T正是因为它是当wait被调用的方法。
*
* <p>线程也可以唤醒,而不会被通知,中断或超时,即所谓的虚假唤醒 。
* 虽然这在实践中很少会发生,但应用程序必须通过测试应该使线程被唤醒的条件来防范,并且如果条件不满足则继续等待。
* 换句话说,等待应该总是出现在循环中,就像这样:
* <pre>
* synchronized (obj) {
* while (<condition does not hold>)
* obj.wait(timeout);
* ... // Perform action appropriate to condition
* }
* </pre>
* <p>如果当前线程interrupted任何线程之前或在等待时,那么InterruptedException被抛出。
* 如上所述,在该对象的锁定状态已恢复之前,不会抛出此异常。
* <p>请注意, wait方法,因为它将当前线程放入该对象的等待集,仅解锁此对象;
* 当前线程可以同步的任何其他对象在线程等待时保持锁定。
* <p>该方法只能由作为该对象的监视器的所有者的线程调用。
* 有关线程可以成为监视器所有者的方法的说明,请参阅notify方法。
*
* @param timeout 等待的最长时间(以毫秒为单位)。
* @throws IllegalArgumentException 如果timeout值为负。
* @throws IllegalMonitorStateException 如果当前线程不是此对象的监视器的所有者
* @throws InterruptedException 如果任何线程在当前线程等待通知之前或当前线程中断当前线程。 当抛出此异常时,当前线程的中断状态将被清除。
*/
public final native void wait(long timeout) throws InterruptedException;
这是官方注解的谷歌翻译。
上面的官方文档的每句话都蛮重要的。。。当然最重要的就两点:
- 使用 wait 的方法前提: 当前线程必须持有本身对象监视器
- 从 wait 唤醒的方式:
- 其他线程调用该对象的 notify 或 notifyAll 方法。
- 其他线程 interrupts 此线程。
- 休眠时间已经过去,线程重新等待调度。如果timeout为零,则不考虑实时,线程等待直到通知。
notify 方法则有以下两个:
/**
* 唤醒正在等待对象监视器的单个线程。
* 如果任何线程正在等待这个对象,其中一个被选择被唤醒。
* 选择是任意的,并且由实施器判断发生。
* 线程通过调用wait方法之一等待对象的监视器。
*
* <p>唤醒的线程将无法继续,直到当前线程放弃此对象上的锁定为止。
* 唤醒的线程将以通常的方式与任何其他线程竞争,这些线程可能正在积极地竞争在该对象上进行同步;
* 例如,唤醒的线程在下一个锁定该对象的线程中没有可靠的权限或缺点。
*
* <p>该方法只能由作为该对象的监视器的所有者的线程调用。 线程以三种方式之一成为对象监视器的所有者:
* <ul>
* <li>通过执行该对象的同步实例方法。
* <li>通过执行在对象上synchronized synchronized语句的正文。
* <li>对于类型为Class,的对象,通过执行该类的同步静态方法。
* </ul>
* <p>一次只能有一个线程可以拥有一个对象的显示器。
*
* @throws IllegalMonitorStateException 如果当前线程不是此对象的监视器的所有者
*/
public final native void notify();
/**
* 唤醒正在等待对象监视器的所有线程。 线程通过调用wait方法之一等待对象的监视器。
* <p>唤醒的线程将无法继续,直到当前线程释放该对象上的锁。
* 唤醒的线程将以通常的方式与任何其他线程竞争,这些线程可能正在积极地竞争在该对象上进行同步;
* 例如,唤醒的线程在下一个锁定该对象的线程中不会有可靠的特权或缺点。
* <p>该方法只能由作为该对象的监视器的所有者的线程调用。
* 有关线程可以成为监视器所有者的方法的说明,请参阅notify方法。
* @throws IllegalMonitorStateException 如果当前线程不是此对象的监视器的所有者
*/
public final native void notifyAll();
整个栗子测试下:
public class TestMain {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
WaitNotify example = new WaitNotify();
executorService.execute(example::waitObj);
executorService.execute(example::notifyObj);
executorService.shutdown();
System.out.println("main 主线程结束");
}
public static class WaitNotify {
public synchronized void notifyObj() {
System.out.println("notity 调用");
notifyAll();
}
public synchronized void waitObj() {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wait 结束");
}
}
}
若启动多个线程调用 wait() 方法,则如下图:
PS: 没有设置守护线程,因此 main 方法将永远不会终结。
这里顺便补充下,sleep()
和 wait()
的区别:
sleep() | wait() |
---|---|
Thread 的静态方法 | Object 的方法 |
保留锁 | 释放锁 |
join
join 方法的作用是使所属的线程对象x正常执行 run() 方法中的任务,而使当前线程z进行无限期的阻塞,等待线程x销毁后再继续执行线程z后面的代码。
join 与 synchronized 的区别是:join 在内部使用 wait()方法进行等待,而 synchronized 关键字使用的是 JVM 底层,使用“对象监视器”原理作为同步。
先来看下 join
方法的源码,其他两个无参和带参方法,最终调用的都是这个 synchronized
方法:
/**
* @param millis 等待时间(以毫秒为单位)
* @throws IllegalArgumentException 如果{@code millis}的值为负
* @throws InterruptedException 如果有任何线程中断了当前线程,抛出此异常时,线程的中断状态将被清除。
*/
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) {
//如果线程未死亡,则循环调用 wait
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
//第一次进入,now 为0,等待 millis 毫秒
//第二次进入,now 为已经等待时间,delay小于等于0时跳出
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
从源码中可以发现, join
最后还是基于 wait
方法实现的。
先看个例子,看看如何工作,再画图:
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
Thread.sleep(2000);
System.out.println("join 线程的 run 方法");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
for (int i = 0; i < 10; i++) {
if (i == 5){
thread.join(1999);
}else {
System.out.println(System.currentTimeMillis() + ", main 线程循环中:" + i);
}
}
}
运行结果,省略前后:
...
1596276428638, main 线程循环中:4
1596276430640, main 线程循环中:6
join 线程的 run 方法
1596276430640, main 线程循环中:7
...
再放张图,大概示意下工作流程:
await、signal
java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。
这种方式可以在一个 Lock 对象里面可以创建多个Condition(即对象监视器)实例,线程对象可以注册在指定的Condition中,从而可以有选择性地对指定线程进行通知,所以更加灵活。
这种方式先贴一个大佬的例子,内容有点多,以后再说,我感觉以后肯定会讲到,应该会有专门一篇。
class AwaitTest {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void before() {
lock.lock();
try {
System.out.println("before");
condition.signalAll();
} finally {
lock.unlock();
}
}
public void after() {
lock.lock();
try {
condition.await();
System.out.println("after");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
AwaitTest example = new AwaitTest();
executorService.execute(example::after);
executorService.execute(example::before);
executorService.shutdown();
}
}
线程中断机制
一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。
线程中断是一个非常重要的概念,中断线程是取消任务最好的方法。
在 Java 的 Thread
类中,提供了以下三个关于线程中断的 public
方法:
void interrupt()
,中断线程。static boolean interrupted()
,测试当前线程是否已被中断。通过此方法可以清除线程的中断状态。boolean isInterrupted()
,测试线程是否已经中断,线程的中断状态不受该方法的影响。
这三个是线程的公共方法,但是它的最底层是两个 native 方法:
private native void interrupt0(); //中断线程
/**
* 测试某些线程是否已被中断。线程的中断状态不受此方法的影响。
* ClearInterrupted参数决定线程中断状态是否被重置,若为true则重置。
*/
private native boolean isInterrupted(boolean ClearInterrupted);
从这里就可以发现:
协作式和抢占式
然后,就有了下一个问题:
中断状态是个啥玩意儿???线程中断难道不是终止线程嘛?为什么还会有状态?Thread 类里面也没有这个状态变量啊,难道操作系统内给线程添加了这个状态?
所以,又又又又得百度去咯。。。根据各位大佬们的博客,个人感觉应该是这样的:
- 明确线程中断状态不是 Thread 类的标志位,而是操作系统中对线程的中断标志。
这一结论来自 native
方法:
/**
* 测试某些线程是否已被中断。线程的中断状态不受此方法的影响。
* ClearInterrupted参数决定线程中断状态是否被重置,若为true则重置。
*/
private native boolean isInterrupted(boolean ClearInterrupted);
很明显了,在操作系统对线程的管理中,确实存在一个中断状态的标志位。
- 操作系统调度线程的方式:协作式、抢占式
- 协作式:线程自己的工作执行完后,要主动通知调度切换到另一个线程上。 如果一个线程编写有问题,一直不告知系统进行线程切换,那么程序就会一直阻塞在那里。线程的执行时间由自身掌控。
- 抢占式:线程将由调度来分配执行时间,线程的切换不由线程自身决定。 不会出现一个线程导致整个进程阻塞的问题。
isInterrupted()
测试线程是否已经中断,线程的中断状态不受该方法的影响。
/**
* 测试此线程是否已被中断。线程的中断状态不受此方法的影响。
* <p>如果中断时,线程并没有存活,那么该方法返回 false。意思就是,如果线程还没有 start 启动,或者已经消亡,那么返回依然是 false.
* @return 如果该线程已被中断,返回true;否则返回 false
*/
public boolean isInterrupted() {
//不清除中断状态
return isInterrupted(false);
}
这是最基础的判断中断状态,且不更改 OS 线程中断标志位。可以对照上面的 native 方法阅读。特别的记一下这两句:
举个栗子看下效果就行:
public static class Example extends Thread{
@Override
public void run() {
while (!isInterrupted()){
}
System.out.println("线程中断");
}
}
然后 main 启动:
public static void main(String[] args) throws Exception {
Example example = new Example();
System.out.println("当前线程 example 尚未启动,此时状态:" + example.isInterrupted());
example.start();
System.out.println("当前线程 example 启动且尚未调用 interrupt,此时状态:" + example.isInterrupted());
example.interrupt();
System.out.println("当前线程 example 启动且调用 interrupt,此时状态:" + example.isInterrupted());
Thread.sleep(5000);
System.out.println("等待 5 秒,example 应该已经消亡,此时状态:" + example.isInterrupted());
}
输出状态为:
当前线程 example 尚未启动,此时状态:false
当前线程 example 启动且尚未调用 interrupt,此时状态:false
当前线程 example 启动且调用 interrupt,此时状态:true
线程中断
等待 5 秒,example 应该已经消亡,此时状态:false
interrupt()
中断线程
先看下源码,以及官方(我自个的渣渣翻译)注释:
/**
* 中断此线程。
* <p>线程可以中断自身,这是允许的。在这种情况下,不用进行安全性验证({@link #checkAccess() checkAccess} 方法检测)
* <p>若当前线程由于 wait() 方法阻塞,或者由于join()、sleep()方法,然后线程的中断状态将被清除,并且将收到 {@link InterruptedException}。
* <p>如果线程由于 IO操作({@link java.nio.channels.InterruptibleChannel InterruptibleChannel})阻塞,那么通道 channel 将会关闭,
* 并且线程的中断状态将被设置,线程将收到一个 {@link java.nio.channels.ClosedByInterruptException} 异常。
* <p>如果线程由于在 {@link java.nio.channels.Selector} 中而阻塞,那么线程的中断状态将会被设置,它将立即从选择操作中返回。
*该值可能是一个非零值,就像调用选择器的{@link java.nio.channels.Selector#wakeupakeup}方法一样。
*
* <p>如果上述条件均不成立,则将设置该线程的中断状态。</p>
* <p>中断未运行的线程不必产生任何作用。
* @throws SecurityException 如果当前线程无法修改此线程
*/
public void interrupt() {
//如果调用中断的是线程自身,则不需要进行安全性判断
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // 只是设置中断标志
b.interrupt(this);
return;
}
}
interrupt0();
}
这个方法的作用,在上面的栗子中已经有过体现了,接着来测试下源码中提到的异常:
public static void main(String[] args) throws Exception {
Thread thread = new Thread(() -> {
try {
Thread.sleep(2000);
System.out.println("线程 sleep 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
thread.interrupt();
System.out.println("main 线程调用 interrupt 结束");
System.out.println("线程抛出了中断异常,此时状态:" + thread.isInterrupted());
Thread.sleep(5000);
System.out.println("等待 5 秒,线程应该已经消亡,此时状态:" + thread.isInterrupted());
}
运行结果:
main 线程调用 interrupt 结束
线程抛出了中断异常,此时状态:true
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at thread.InterruptTest.lambda$test2$0(InterruptTest.java:20)
at thread.InterruptTest$$Lambda$1/791452441.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
等待 5 秒,线程应该已经消亡,此时状态:false
在 sleep 过程中调用中断,抛出中断异常。。线程不会往下执行。
虽然抛出了异常,但是线程的中断状态确实设置成功了。。。只是线程被立刻从 sleep 中唤醒。
嗯,这两这个好理解。。。。。
interrupted()
测试当前线程是否已被中断。通过此方法可以清除线程的中断状态。
/**
* 测试当前线程是否已被中断。
* 通过此方法可以清除线程的中断状态.
* 换句话说,如果此方法要连续调用两次,则第二个调用将返回false(除非当前线程在第一个调用清除了它的中断状态之后,且在第二个调用对其进行检查之前再次中断)
* <p>如果中断时,线程并没有存活,那么该方法返回 false
* @return 如果该线程已被中断,返回true;否则返回 false
*/
public static boolean interrupted() {
//清除线程的中断状态
return currentThread().isInterrupted(true);
}
说实话,这个方法的作用,实在是有点迷。。。。
public static class Example2 extends Thread{
@Override
public void run() {
System.out.println("调用 interrupt 方法,线程中断");
interrupt();
System.out.println("尚未调用 interrupted 方法,此时线程中断状态:" + isInterrupted());
System.out.println("线程第 1 次调用 interrupted 方法,方法返回:" + Thread.interrupted());
System.out.println("线程第 1 次调用 interrupted 方法结束后,此时线程中断状态:" + isInterrupted());
System.out.println("线程第 2 次调用 interrupted 方法,方法返回:" + Thread.interrupted());
System.out.println("线程第 2 次调用 interrupted 方法结束后,此时线程中断状态:" + isInterrupted());
System.out.println("线程第 3 次调用 interrupted 方法,方法返回:" + Thread.interrupted());
System.out.println("线程第 3 次调用 interrupted 方法结束后,此时线程中断状态:" + isInterrupted());
}
}
mian 方法测试:
public static void main(String[] args) throws Exception {
Example2 example = new Example2();
example.start();
}
控制台输出结果为:
调用 interrupt 方法,线程中断
尚未调用 interrupted 方法,此时线程中断状态:true
线程第 1 次调用 interrupted 方法,方法返回:true
线程第 1 次调用 interrupted 方法结束后,此时线程中断状态:false
线程第 2 次调用 interrupted 方法,方法返回:false
线程第 2 次调用 interrupted 方法结束后,此时线程中断状态:false
线程第 3 次调用 interrupted 方法,方法返回:false
线程第 3 次调用 interrupted 方法结束后,此时线程中断状态:false
乍一看,感觉需要挠头。。。这个方法的作用也忒不明显了。。。 java doc
也不讲的不清楚。
仔细一想,是这样的:
- 作为一个
static
方法,作用于当前线程 - 调用
static boolean interrupted()
方法后,先将线程的中断状态设置为 false,再返回原先的中断状态
参考文章
https://www.cnblogs.com/javadevelper/p/6036472.html
https://www.pdai.tech/md/java/thread/java-thread-x-thread-basic.html
https://blog.csdn.net/qq_33565047/article/details/102958254
https://blog.csdn.net/wanliguodu/article/details/81005560
https://www.cnblogs.com/trust-freedom/p/6606594.html
https://blog.csdn.net/qq_22771739/article/details/82529874
https://blog.csdn.net/weixin_33782386/article/details/92423372
https://segmentfault.com/a/1190000016056471
https://www.cnblogs.com/JianGuoWan/p/9139698.html
https://segmentfault.com/a/1190000019962661
https://www.jianshu.com/p/beb5413c5ce6
https://www.cnblogs.com/Donnnnnn/p/7234934.html
https://blog.csdn.net/oldshaui/article/details/106952102
https://blog.csdn.net/qq_41901915/article/details/103654263
https://www.cnblogs.com/L-a-u-r-a/p/8575217.html
https://blog.csdn.net/tianjindong0804/article/details/105134182