一. 线程实现
HotSpot中的每个线程都是直接映射到一个操作系统原生线程来实现的,而且中间没有额外的间接结构,所以HotSpot不会去干涉线程调度的(可以设置线程优先级给操作系统提供调度建议,优先级不会严格保证顺序),全权交给底下的操纵系统去处理,所以何时冻结或唤醒线程、该给线程分配多少处理器执行时间、该把线程安排给哪个处理器核心去执行等,都是由操作系统完成的。
二. 状态转换
Java语言定义了6种线程状态,在任意一个时间点中,一个线程只能有且只有其中的一个状态,并且可以通过特定的方法在不同状态之间转换。这6种状态分别是:
- 新建(
New
): 创建后尚未启动的线程处于这种状态。 - 运行(
Runnable
): 包括操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着操作系统为它分配执行时间。 - 无限期等待(
Waitting
): 处于这种状态的线程不会被分配处理器执行时间,要等待被其他线程显式唤醒。以下方法会让线程陷入无限期的等待状态:
- 没有设置timeout参数的
Object::wait()
方法 - 没有设置timeout参数的
Thread::join()
方法 LockSupport::park()
方法
- 限期等待(
Timed Waiting
): 处于这种状态的线程也不会被分配处理器执行时间,不过无须等待被其他线程显示唤醒,在一定时间后它们会由系统自动唤醒。以下方法会让线程进入限期等待状态:
Thread::sleep()
方法- 设置了timeout参数的
Object::wait()
方法 - 设置了timeout参数的
Thread::join()
方法 LockSupport::parkNanos()
方法LockSupport::parkUntil()
方法
- 阻塞(
Blocked
): 线程被阻塞了,“阻塞状态”与“等待状态”的区别是“阻塞状态”在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。 - 结束(
Terminated
): 已终止线程的线程状态,线程已经结束执行。
上述6种状态在遇到特定事件发生的时候会互相转换,转换关系如下图:
三. 线程中断
3.1 核心方法
在Java的线程Thread类中有三个方法与线程中断相关,比较容易混淆。
-
this.interrupt()
:发出一个中断请求,把标志位设定为中断状态,并不会使得线程停止。 -
this.isInterrupted()
:测试Thread实例对象是否已经是中断状态,但不清除状态标志。 -
Thread.interrupted()
:Thread类的静态方法,测试当前线程是否已经是中断状态,执行后将状态标志清除,重新置为false。当前进程是指运行代码的进程,例如在main函数中调用,则当前进程即为main线程。
3.2 响应中断
一般来说,阻塞函数,如Thread.sleep
、Thread.join
、Object.wait
等在检查到线程的中断状态时,会抛出InterruptedException
,同时会清除线程的中断状态,即重新设置为false。
synchronized
在获锁的过程中是不能被中断的,意思是说如果产生了死锁,则不可能被中断。与synchronized
功能相似的reentrantLock.lock()
方法也是一样,它也不响应中断,即如果发生死锁,那么reentrantLock.lock()
方法无法终止,如果调用时被阻塞,则它一直阻塞到它获取到锁为止。
但是如果调用带超时的tryLock
方法 reentrantLock.tryLock(long timeout, TimeUnit unit)
,那么如果线程在等待时被中断,将抛出一个 InterruptedException
异常,这是一个非常有用的特性,因为它允许程序打破死锁。你也可以调用reentrantLock.lockInterruptibly()
方法,它就相当于一个超时设为无限的tryLock
方法。
3.3 判定线程是否是中断状态
Thread类提供了两个方法:
Thread.interrupted()
: 静态方法,检测的是当前线程是否中断。该方法会清除中断标志。
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
this.isInterrupted()
: 实例方法,检测Thread实例对象是否中断。该方法不会清除中断标志。
private native boolean isInterrupted(boolean ClearInterrupted);
下面通过代码示例来深入理解两个方法的区别:
【示例1】
public class MyThread extends Thread {
private final long start = System.currentTimeMillis();
@Override
public void run() {
while (System.currentTimeMillis() - start <= 2000) {
}
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
thread.interrupt();
System.out.println("是否停止1:" + thread.interrupted());
System.out.println("是否停止2:" + thread.interrupted());
}
}
输出结果为:
//输出为false,因为interrupted检测的是main线程,而不是thread线程。
是否停止1:false
是否停止2:false
将最后的两行代码改为:
System.out.println("是否停止1:" + thread.isInterrupted());
System.out.println("是否停止2:" + thread.isInterrupted());
则输出结果为:
是否停止1:true
//仍为true,证明isInterrupted方法不会清除中断标志
是否停止2:true
【示例2】
public class MyThread extends Thread {
@Override
public void run() {
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.start();
thread.interrupt();
Thread.sleep(100);
System.out.println("是否停止1:" + thread.isInterrupted());
System.out.println("是否停止2:" + thread.isInterrupted());
} catch (InterruptedException e) {
}
}
}
输出结果为:
是否停止1:false
是否停止2:false
之所以输出结果为false
,是因为线程处于可响应中断的阻塞状态时,若被中断会抛出InterruptedException
异常,并且会清除掉中断标志。
四. 线程停止
严禁使用stop
方法停止线程,原因如下:
- 可能使一些清理性的工作无法完成;
- 对锁定的对象进行解锁,导致数据得不到同步的处理,出现数据不一致的问题。
一般有两种方式来正确地停止线程:
- 使用中断加异常捕获的方式
- 使用
volatile
标志位的方式
4.1 通过中断停止线程
public class MyThread extends Thread {
@Override
public void run() {
try {
while (true) {
if (interrupted()) {
System.out.println("线程发生中断");
throw new InterruptedException();
}
}
} catch (InterruptedException e) {
System.out.println("进入MyThread类run方法的catch语句");
}
}
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.start();
thread.interrupt();
thread.join();
System.out.println("end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
使用中断停止线程要注意以下场景:
public void run() {
while (!isInterrupted()) {
try {
sleep(1000);
} catch (InterruptedException e) {
System.out.println("进入MyThread类run方法的catch语句");
}
}
}
上述代码发生中断仍旧无法停止,因为线程处于可响应中断的阻塞状态时,若被中断会抛出InterruptedException
异常,并且会清除掉中断标志,导致无法跳出while循环。正确的做法是将循环写到try语句内,这样发生中断后就会跳出循环,进入catch语句。
4.2 使用volatile
标志位停止线程
public class MyThread extends Thread {
private volatile boolean stop = false;
@Override
public void run() {
while (true) {
if (stop) {
System.out.println("线程停止");
return;
}
}
}
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(1000);
thread.stop = true;
thread.join();
System.out.println("end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
五. 暂停及恢复
suspend
和resume
方法也已废弃,主要是容易造成公共的同步对象的独占,使得其他线程无法访问公共同步对象。因为suspend
并不会使得线程释放锁,容易造成死锁,同时也容易造成数据的不同步。
例如以下例子,线程thread1
修改了变量name
之后调用suspend
,导致变量pwd
未被改变,此时线程thread2
将读取到name=a,pwd=11
的错误数据。
public class MyObject {
private String name = "1";
private String pwd = "11";
public void setValue(String name, String pwd) {
this.name = name;
if (Thread.currentThread().getName().equals("a")) {
System.out.println("停止a线程");
Thread.currentThread().suspend();
}
this.pwd = pwd;
}
public void print() {
System.out.println(name + " " + pwd);
}
}
public static void main(String[] args) throws InterruptedException {
final MyObject myObject = new MyObject();
Thread thread1 = new Thread(() -> myObject.setValue("a", "aa"));
thread1.setName("a");
thread1.start();
Thread.sleep(500);
Thread thread2 = new Thread(() -> myObject.print());
thread2.start();
}
六. 线程通信
6.1 wait/notify机制
wait()
方法是Object
类的方法,在调用之前需要获得++所在对象++的对象锁,即需要在synchronized
方法或synchronized
语句块中调用,否则会抛出IllegalMonitorStateException
异常。在执行wait()
方法之后,当前线程会释放锁,并进入等待状态,当被其他线程唤醒后,仍然需要重新获得锁才能往下执行。
notify()
方法也需要获得所在对象的对象锁,否则也会抛出IllegalMonitorStateException
异常。如果有多个线程等待,则挑选出其中一个呈wait
状态的线程,对其发出通知notify
。需要注意的是,notify
方法并不会释放锁,需要退出synchronized
代码后线程才会释放锁,而被唤醒的线程此时需要重新获取锁,才能执行剩余代码逻辑。
代码示例如下:
public class ThreadA extends Thread {
private Object lock;
public ThreadA(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
try {
System.out.println(getName() + "开始等待:" + System.currentTimeMillis());
lock.wait();
System.out.println(getName() + "结束等待:" + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadB extends Thread {
private Object lock;
public ThreadB(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
try {
System.out.println(getName() + "开始等待:" + System.currentTimeMillis());
lock.notify();
sleep(5000);
System.out.println(getName() + "结束等待:" + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class App {
public static void main(String[] args) {
try {
Object lock = new Object();
ThreadA threadA = new ThreadA(lock);
threadA.setName("ThreadA");
threadA.start();
Thread.sleep(100);
ThreadB threadB = new ThreadB(lock);
threadB.setName("ThreadB");
threadB.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果输出如下:
ThreadA开始等待:1609056243352
ThreadB开始等待:1609056243456
ThreadB结束等待:1609056248461
ThreadA结束等待:1609056248462
6.2 Lock的Condition机制
关键字synchronized
与wait/notify/notifyAll
方法结合可以实现等待/通知功能,而Lock
结合Condition
对象也可以实现等待/通知模型。一个Lock
对象里面可以创建多个Condition
对象,可以实现“选择性通知”。
与wait/notify
类似的是,调用Condition
对象的await
方法之前,需要通过Lock
对象的lock
方法获得锁,否则也会抛出IllegalMonitorStateException
异常。
简单代码示例如下:
public class MyService {
private Lock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
public void awaitA() {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "开始等待A:" + System.currentTimeMillis());
conditionA.await();
System.out.println(Thread.currentThread().getName() + "结束等待A:" + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signalA() {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "开始唤醒A:" + System.currentTimeMillis());
conditionA.signal();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "结束唤醒A:" + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void awaitB() {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "开始等待B:" + System.currentTimeMillis());
conditionB.await();
System.out.println(Thread.currentThread().getName() + "结束等待B:" + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signalB() {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "开始唤醒B:" + System.currentTimeMillis());
conditionB.signal();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "结束唤醒B:" + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class ThreadA extends Thread {
private MyService service;
public ThreadA(MyService service) {
this.service = service;
}
@Override
public void run() {
service.awaitA();
}
}
public class ThreadB extends Thread {
private MyService service;
public ThreadB(MyService service) {
this.service = service;
}
@Override
public void run() {
service.awaitB();
}
}
public class App {
public static void main(String[] args) {
try {
MyService service = new MyService();
ThreadA threadA = new ThreadA(service);
threadA.setName("ThreadA");
threadA.start();
ThreadB threadB = new ThreadB(service);
threadB.setName("ThreadB");
threadB.start();
Thread.sleep(100);
service.signalA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果如下:
ThreadB开始等待B:1609058045382
main开始唤醒A:1609058045488
main结束唤醒A:1609058046489
ThreadA结束等待A:1609058046490
//ThreadB未被唤醒,等待中...
综上所述,Condition
对象的await/signal
方法的使用与Object
对象的wait/notify
方法的使用是相当类似的,区别在于一个Lock
对象可以创建多个Condition
对象,从而达到“精确控制”的功能,而Object
对象相当于只有一个Condtion
对象的情况。
七. 参考资料
- 《深入理解Java虚拟机第三版》
- 《Java多线程编程核心技术》