目录
方法二:Condition类中的await和signalAll方法
方法三:LockSupport类中的park等待和unpark唤醒
一.线程中断机制
假设从网络下载一个100M的文件,如果网速很慢,用户等得不耐烦,就可能在下载过程中点“取消”,这时,程序就需要中断下载线程的执行。
1.1如何中断运行中的线程?
通过一个volatile变量实现
在多线程编程中,可见性是指当一个线程修改了共享变量的值时,其他线程能够立即看到这个修改。在 Java 中,由于线程之间存在本地缓存,为了确保可见性,我们可以使用 volatile 关键字。
使用 volatile 关键字能够告诉 JVM 不要对这个变量进行本地缓存优化,而是每次都从主内存中读取变量的值。这样,当一个线程修改了 isStop 的值时,其他线程能够立即看到这个修改,确保了可见性。
使用原子操作类
其实原子操作类中的属性value也是通过volatile修饰来保证可见性的。
通过Thread类自带的中断API方法实现
一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。
Java提供了一种用于停止线程的协商机制——中断。 中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。
- 若要中断一个线程,你需要手动调用该线程的 interrupt 方法,该方法也仅仅是将线程对象的中断标识设成 true,并不是真正立刻停止线程;
- 接着你需要自己写代码不断地检测当前线程的标识位,如果为 true,表示别的线程要求这条线程中断,此时究竟该做什么需要你自己写代码实现。
Thread类定义了如下关于中断的方法:
1.2Thread类的三大API说明
实例方法interrupt(),没有返回值
当对一个线程,调用 interrupt() 时:
- 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。所以, interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。
- 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么当前线程将立即退出被阻塞状态,并抛出一个InterruptedException异常,并且会清除它的中断状态,即false。
- 中断一个不活动的线程不会产生任何影响。
调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中(例如处于sleep, wait, join 等状态)的线程。lock.lockInterruptibly() 方法是可中断的获取锁。即获取不到锁的线程能够响应中断,不是死等,当获取不到锁的线程被其它线程中断时,中断异常被抛出。而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
实例方法isInterrupted(),返回布尔值
测试此线程是否已中断。这个实例方法的底层调用了一个native方法,传入了一个布尔值,而这个值就是 是否清除中断标识位,false表示不清除,true表示清除(即将线程的中断标识位清除重新设置为false)。
静态方法 interrupted(),返回布尔值
Thread.interrupted();判断线程是否被中断,并清除当前中断状态这个方法做了两件事:
- 返回当前线程的中断状态
- 将当前线程的中断状态设为 false
1.3sleep响应中断
线程中常用的阻塞方法,如sleep,join和wait 都会响应中断,然后抛出一个中断异常 InterruptedException。但是,注意此时,线程的中断状态会被清除。
sleep()实现的伪代码
sleep(){
if(中断状态 == true) {
中断状态 = false;
throw new InterruptedException();
}
线程开始睡觉;
if(中断状态 == true) {
中断状态 = false;//清除中断状态
throw new InterruptedException();
}
}
所以,当我们捕获到中断异常之后,应该保留中断信息,以便让上层代码知道当前线程中断了。通常有两种方法可以做到:
- 一种是,捕获异常之后,再重新抛出异常,让上层代码知道。
- 另一种是,在捕获异常时,通过 interrupt 方法把中断状态重新设置为true。
下面,就以sleep方法为例,捕获中断异常,然后重新设置中断状态:
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (Exception e) {
System.out.println(Thread.currentThread().getName()+"线程第一次中断标志:"+Thread.currentThread().isInterrupted());
//重新把线程中断状态设置为true,以便上层代码判断
Thread.currentThread().interrupt();
System.out.println(Thread.currentThread().getName()+"线程第二次中断标志:"+Thread.currentThread().isInterrupted());
}
}
});
t.start();
Thread.sleep(100);
t.interrupt();
1.4park等待和interrupt中断
如果中断状态为true,那么park()方法会直接返回,不会阻塞(会消耗permit)。注意,这里并不会清除中断标志。
interrupt()实现的伪代码
interrupt(){
if(中断状态 == false) {
中断状态 = true;
}
unpark(this); //注意这是Thread的成员方法,所以我们可以通过this获得Thread对象
}
interrupt()会设置中断状态为true。注意,interrupt()还会去调用unpark的,所以也会把permit置为1的。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("interrupt t1");
Thread.currentThread().interrupt();
System.out.println(Thread.currentThread().isInterrupted()); // true
System.out.println(Thread.interrupted()); // true
System.out.println(Thread.currentThread().isInterrupted()); // false
System.out.println("park t1");
// 此时中断标志已经为false,但线程不阻塞,因为之前 interrupt() 会将 permit 置为1
LockSupport.park();
System.out.println("第一次 park t1 没有阻塞");
System.out.println("t1 再次 park 阻塞");
LockSupport.park(); // 线程阻塞
System.out.println("第二次 park t1 阻塞");
}, "t1");
t1.start();
TimeUnit.MILLISECONDS.sleep(500); // 确保 t1 执行
}
“如果中断状态为true,那么park无法阻塞”。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("interrupt t1");
Thread.currentThread().interrupt();
System.out.println(Thread.currentThread().isInterrupted()); // true
System.out.println(Thread.currentThread().isInterrupted()); // true
System.out.println("park t1");
//此时中断标志已经为true时,park无法阻塞
LockSupport.park();
System.out.println("第一次 park t1 没有阻塞");
System.out.println("t1 再次 park 阻塞");
LockSupport.park(); // 线程阻塞
System.out.println("第二次 park t1 没有阻塞");
}, "t1");
t1.start();
TimeUnit.MILLISECONDS.sleep(500); // 确保 t1 执行
}
二.线程之间的通信(等待唤醒机制)
方法一:Object类中的wait和notifyAll方法
需要使用synchronized关键字
- notify:唤醒等待队列中第一个等待线程(等待时间最长的线程),使其从wait()方法返回,而返回的前提时该线程获取到对象的锁。
- notifyAll:通知所有等待在该对象上的线程。notify()/notifyAll() 只能唤醒等待在同一把锁上的线程。
- wait:
- wait在不传递任何参数的情况下会进入 waiting状态,当wait(long)里面设置了一个大于0的整数时,它会进入timed_waiting,与sleep一样,但sleep不释放锁,而wait释放锁。
- 当调用wait不传递参数的时候,其底层实现也是调用了 wait(0)这个方法,wait(0)表示一直休眠。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。
- 调用wait方法的线程将进入阻塞等待状态,并且会被加入到一个等待队列,只有等待另外线程的通知或者被中断才会返回。
为什么wait会释放锁?而sleep不会释放锁?
sleep必须要传递一个最大等待时间的,就说 sleep是可控的(对于时间层面来讲),而wait是可以不传递传输,不可控的。从设计层面来讲,如果让 wait这个没有超时等待时间的机制不释放锁的话,那么线程可能会一直阻塞,而sleep 就不存在这个问题。
sleep(0) vs wait(0)有什么区别?
- sleep(0)表示过0毫秒之后继续执行,而wait(0)表示一直休眠。
- sleep(0)表示重新触发一次CPU竞争(抢占式执行)。
- sleep(0)会进入timed_waiting状态,而wait(0)进入waiting状态。
为什么wait是 0bject的方法,而sleep是Thread 的方法?
wait需要操作锁,而锁是属于对象级别(所有的锁都是放在对象头当中),而不是线程级别。一个线程中可以有多把锁,为了灵活起见,所以就将wait 放在0bject 当中。而sleep是不需要操作锁的。
均是Object的方法,均只能在同步方法或者同步代码块中使用,否则会抛出异常IIIegalMonitorStageException
//第一步 创建资源类,定义属性和操作方法
class Share1 {
//初始值
private int number = 0;
//+1的方法
public synchronized void incr() throws InterruptedException {
//第二步 判断 干活 通知
if (number != 0) { //判断number值是否是0,如果不是0,等待
this.wait(); //在哪里睡,就在哪里醒
}
//如果number值是0,就+1操作
number++;
System.out.println(Thread.currentThread().getName() + " :: " + number);
//通知其他线程
this.notifyAll();
}
//-1的方法
public synchronized void decr() throws InterruptedException {
//判断
if (number != 1) {
this.wait();
}
//干活
number--;
System.out.println(Thread.currentThread().getName() + " :: " + number);
//通知其他线程
this.notifyAll();
}
}
public class ThreadDemo1 {
//第三步 创建多个线程,调用资源类的操作方法
public static void main(String[] args) {
Share1 share = new Share1();
//创建线程
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
share.incr(); //+1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "AA").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
share.decr(); //-1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "BB").start();
}
}
虚假唤醒问题
上面的例子中是两个线程,我此时再创建一个线程 cc
接下来我们来分析一下这段代码为什么会出现负数的问题。
- 假设某一时刻,number 为 0 ,B、C两个消费者线程按顺序(因为加锁的缘故)调用 decrement 都发现 number 为 0,就都会调用 wait 方式进行释放锁进行等待;
- 然后线程A也调用 increment,判断是0,不满足调用wait条件,然后将 number 加成1之后,调用notifyAll方法同时唤醒B、C线程,A执行完代码,释放了锁;
- B、C被唤醒之后,假设B抢到锁,C没抢到,C继续阻塞,B从wait方法那继续往下走,将number 减1,此时number 变为 0
- B执行完释放了锁之后C这时抢到了锁,也从wait方法那继续执行代码,然后也将number 减1,这下出现问题了,线程B减完之后就是0了,线程C又将number=0减1,那不就变成-1了,所以这就产生的负数的情况。
虚假唤醒就是由于把所有线程都唤醒了,但是只有其中一部分是有用的唤醒操作,其余的唤醒都是无用功,对于不应该被唤醒的线程而言,便是虚假唤醒。
解决方法
很简单,在等待方执行的逻辑中,一定要用while循环来判断等待条件。
因为执行notify/notifyAll方法时只是让等待线程从wait方法返回,而非重新进入临界区
方法二:Condition类中的await和signalAll方法
需要使用Lock锁
在等待方执行的逻辑中,一定要用while循环来判断等待条件。
//第一步 创建资源类,定义属性和操作方法
class Share2 {
private int number = 0;
//创建Lock
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
//+1
public void incr() throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while (number != 0) {
condition.await();
}
//干活
number++;
System.out.println(Thread.currentThread().getName() + " :: " + number);
//通知
condition.signalAll();
} finally {
//解锁
lock.unlock();
}
}
//-1
public void decr() throws InterruptedException {
lock.lock();
try {
while (number != 1) {
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + " :: " + number);
condition.signalAll();
} finally {
lock.unlock();
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
Share2 share = new Share2();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "AA").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "BB").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "CC").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "DD").start();
}
}
Condition是一个接口,可以使用 lock.newCondition() 来创建实例,Condition的方法如下:
均只能在 lock锁块 中使用,否则会抛出异常IIIegalMonitorStageException
方法三:LockSupport类中的park等待和unpark唤醒
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个许可(permit),permit只有两个值1和零,默认是零。可以把许可看成是一种 (0,1) 信号量(Semaphore),但与 信号量(Semaphore)不同的是,许可的累加上限是1。
- park() /park(Object blocker) :如果有凭证,则会直接消耗掉这个凭证然后正常退出;如果无凭证,就必须阻塞等待凭证可用。
- unpark(Thread thread) :如果给定线程尚不可用,则为其提供许可。但凭证最多只能有1个,累加无效。
优点:
- 不需要获取锁: LockSupport的阻塞和唤醒不需要先获得锁。传统的synchronized和Lock都是基于锁的,线程必须先获得锁才能调用相应的阻塞或唤醒方法。而LockSupport不依赖于任何锁,可以在任意时刻调用。
- 不会抛出异常: LockSupport的阻塞和唤醒操作不会抛出中断异常,因此避免了因为中断而引入的异常处理逻辑。在传统的wait()和await()方法中,线程在等待时可能会被中断,需要捕获InterruptedException,而LockSupport避免了这一点。