JUC并发编程03——线程中断与LockSupport等待唤醒

目录

一.线程中断机制

1.1如何中断运行中的线程?

通过一个volatile变量实现

使用原子操作类

通过Thread类自带的中断API方法实现

1.2Thread类的三大API说明

实例方法interrupt(),没有返回值

实例方法isInterrupted(),返回布尔值

静态方法 interrupted(),返回布尔值

1.3sleep响应中断

1.4park等待和interrupt中断

二.线程之间的通信(等待唤醒机制)

方法一:Object类中的wait和notifyAll方法

虚假唤醒问题

解决方法

方法二: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没有给中断增加任何语法,中断的过程完全需要程序员自己实现。

  1. 若要中断一个线程,你需要手动调用该线程的 interrupt 方法,该方法也仅仅是将线程对象的中断标识设成 true,并不是真正立刻停止线程
  2. 接着你需要自己写代码不断地检测当前线程的标识位,如果为 true,表示别的线程要求这条线程中断,此时究竟该做什么需要你自己写代码实现。

Thread类定义了如下关于中断的方法:

1.2Thread类的三大API说明

实例方法interrupt(),没有返回值

当对一个线程,调用 interrupt() 时:

  1. 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。所以, interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。
  2. 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么当前线程将立即退出被阻塞状态,并抛出一个InterruptedException异常,并且会清除它的中断状态,即false。
  3. 中断一个不活动的线程不会产生任何影响。

调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中(例如处于sleep, wait, join 等状态)的线程。lock.lockInterruptibly() 方法是可中断的获取锁。即获取不到锁的线程能够响应中断,不是死等,当获取不到锁的线程被其它线程中断时,中断异常被抛出。而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

实例方法isInterrupted(),返回布尔值

测试此线程是否已中断。这个实例方法的底层调用了一个native方法,传入了一个布尔值,而这个值就是 是否清除中断标识位,false表示不清除,true表示清除(即将线程的中断标识位清除重新设置为false)。

静态方法 interrupted(),返回布尔值

Thread.interrupted();判断线程是否被中断,并清除当前中断状态这个方法做了两件事:

  1. 返回当前线程的中断状态   
  2. 将当前线程的中断状态设为 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)有什么区别?

  1. sleep(0)表示过0毫秒之后继续执行,而wait(0)表示一直休眠。
  2. sleep(0)表示重新触发一次CPU竞争(抢占式执行)。
  3. 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

接下来我们来分析一下这段代码为什么会出现负数的问题。

  1. 假设某一时刻,number 为 0 ,B、C两个消费者线程按顺序(因为加锁的缘故)调用 decrement 都发现 number 为 0,就都会调用 wait 方式进行释放锁进行等待;
  2. 然后线程A也调用 increment,判断是0,不满足调用wait条件,然后将 number 加成1之后,调用notifyAll方法同时唤醒B、C线程,A执行完代码,释放了锁;
  3. B、C被唤醒之后,假设B抢到锁,C没抢到,C继续阻塞,B从wait方法那继续往下走,将number 减1,此时number 变为 0
  4. 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避免了这一点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值