JUC并发编程03——LockSupport与线程中断

一.线程中断机制

假设从网络下载一个100M的文件,如果网速很慢,用户等得不耐烦,就可能在下载过程中点"取消",这时,程序就需要中断下载线程的执行。

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

通过一个volatile变量实现

在多线程编程中,可见性是指当一个线程修改了共享变量的值时,其他线程能够立即看到这个修改。在 Java 中,由于线程之间存在本地缓存,为了确保可见性,我们可以使用 volatile 关键字。

使用 volatile 关键字能够告诉 JVM 不要对这个变量进行本地缓存优化,而是每次都从主内存中读取变量的值。这样,当一个线程修改了 isStop 的值时,其他线程能够立即看到这个修改,确保了可见性。

使用原子变量类

通过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. 中断一个不活动的线程不会产生任何影响。

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

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

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

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

  1. 返回当前线程的中断状态
  2. 将当前线程的中断状态设为 false

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

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

需要使用synchronized关键字

  • notify唤醒队列中第一个等待线程(等待时间最长的线程),使其从wait()方法返回,而返回的前提时该线程获取到对象的锁。
  • notifyAll :通知所有等待在该对象上的线程。notify()/notifyAll()只能唤醒等待在同一把锁上的线程
  • wait :调用此方法的线程进入阻塞等待状态,并且会被加入到一个等待队列, 只有等待另外线程的通知或者被中断 才会返回,调用wait方法会释放对象的锁

均是Object的方法,均只能在同步方法或者同步代码块中使用,否则会抛出异常IIIegalMonitorStageException

java 复制代码
//第一步 创建资源类,定义属性和操作方法
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循环来判断等待条件。

java 复制代码
//第一步 创建资源类,定义属性和操作方法
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避免了这一点。
相关推荐
全栈独立开发者7 小时前
点餐系统装上了“DeepSeek大脑”:基于 Spring AI + PgVector 的 RAG 落地指南
java·人工智能·spring
dmonstererer7 小时前
【k8s设置污点/容忍】
java·容器·kubernetes
super_lzb7 小时前
mybatis拦截器ParameterHandler详解
java·数据库·spring boot·spring·mybatis
程序之巅7 小时前
VS code 远程python代码debug
android·java·python
我是Superman丶7 小时前
【异常】Spring Ai Alibaba 流式输出卡住无响应的问题
java·后端·spring
墨雨晨曦887 小时前
Nacos
java
invicinble7 小时前
seata的认识与实际开发要做的事情
java
乌日尼乐7 小时前
【Java基础整理】Java多线程
java·后端
2501_941870568 小时前
从配置频繁变动到动态配置体系落地的互联网系统工程实践随笔与多语言语法思考
java·前端·python
她说..8 小时前
Spring 核心工具类 AopUtils 超详细全解
java·后端·spring·springboot·spring aop