【JUC】Java并发编程从挖坑到入土全解(4-一文讲通LockSupport与线程中断->长图预警)

本文主要是介绍【JUC】Java并发编程从挖坑到入土全解(4-一文讲通LockSupport与线程中断->长图预警),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

LockSupport与线程中断

线程中断机制

什么是中断机制?

与中断相关的3个API

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

当前线程的中断标识为true,是不是线程就会立刻停止?

如何理解静态方法Thread.interrupted()

LockSupport是什么

线程等待和唤醒机制

3种让线程等待唤醒的方法

Object类中的wait()和notify()方法实现线程的等待和唤醒

Condition接口中的await()后signal()方法实现线程的等待和唤醒

上述两个对象Object和Condition使用的限制条件

LockSupport类中的park()等待和unpack()唤醒

总结


LockSupport与线程中断

线程中断机制

什么是中断机制?

首先,一个线程不应该由其他线程强制中断或停止,而是应该由线程自己自行停止,自己来决定自己的命运(所以,Thread的stop()、suspend()、resume()都已经废弃了)

其次,在Java中没有办法立即停止一条线程,然而停止线程又显得那么重要(比如需要取消一个耗时/错误操作)。因此,Java提供了一种用于停止线程的协商机制——中断,也即中断标识协商机制

中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完成完全要求程序员自己实现。若要中断一个线程,需要手动调用该线程的interrupt()方法,该方法也仅仅是将线程对象的中断标识设成true,接着按自己的需要,写代码不断监测当前线程的标识位,如果为true,表示别的线程请求这条线程中断,此时究竟该做什么需要自己写代码实现。

每个线程对象中都有一个中断标识位,用于表示线程是否被中断:该标识位为true标识中断,为false表示未中断。通过调用线程对象的interrupt方法将该线程的标识位设为true,可以在别的线程中带调用,也可以在自己的线程中调用。

与中断相关的3个API
  • public void interrupt():设置线程的中断状态为true,发起一个协商而不会立刻停止线程
  • public static boolean isInterrupted():通过检查中断标识位判断线程是否被中断并清除当前中断状态(返回当前线程的中断状态+重置中断状态为false)
  • public boolean isInterrupted():通过检查中断标识位判断当前线程是否被中断
如何停止中断运行中的线程?
  • 通过volatile变量实现(线程间的可见性)
  • 通过AtomicBoolean实现

执行结果如下图:

  • 通过Thread类自带的中断API实例方法实现 在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑stop线程
    • interrupt() 中断线程

我们可以看到interrupt0()是一个native方法

除非线程正在中断(始终允许),否则将带调用此线程的checkAccess()方法(可能还会导致抛出SecurityException)

public class InterruptDemo {static volatile boolean isStop = false;public static void main(String[] args) {Thread tA = new Thread(() -> {while (true) {if (isStop) {System.out.printf(Thread.currentThread().getName() + " isStop 被修改为true!");break;}System.out.println("-------------------->");}}, "tA");tA.start();try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {isStop = true;}, "B").start();}
}

执行结果如下图:

public class InterruptDemo {static AtomicBoolean atomicBoolean = new AtomicBoolean(false);public static void main(String[] args) {Thread tA = new Thread(() -> {while (true) {if (atomicBoolean.get()) {System.out.printf(Thread.currentThread().getName() + " atomicBoolean 被修改为true!");break;}System.out.println("-------------------->");}}, "tA");tA.start();try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {atomicBoolean.set(true);}, "B").start();}
}
package com.aqin.juc;import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;/*** @Description* @Author aqin1012 AQin.* @Date 10/9/23 11:29 AM* @Version 1.0*/
public class InterruptDemo {public static void main(String[] args) {Thread tA = new Thread(() -> {while (true) {if (Thread.currentThread().isInterrupted()) {System.out.printf(Thread.currentThread().getName() + " isInterrupted() 被修改为true!程序停止!");break;}System.out.println("-------------------->");}}, "tA");tA.start();try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {tA.interrupt();}, "B").start();}
}

执行效果如下图:

当前线程的中断标识为true,是不是线程就会立刻停止?

不会,具体来说,对一个线程,调用interrupt()时,如果线程处于正常活动状态,那么会将该线程的中断标志设置为true(仅此而已),被设置中断标志的线程将继续正常运行,不受影响。所以,interrupt()并不是真正的中断线程,需要被调用的线程自己进行配合才行。如果线程处于被阻塞状态(如sleep、wait、join等状态),在别的线程中调用当前线程对象的interrupt()方法时,那么线程将会立即退出被阻塞状态,并抛出一个InterruptedException异常。

如何理解静态方法Thread.interrupted()

这个方法是判断线程是否中断并清除当前中断状态的,简单来说就是“复位”。

主要做了两件事:

  1. 返回当前线程的中断状态,测试当前线程是否已经被中断
  2. 将当前线程的中断状态清零并重新设置为false,清除线程的中断状态

那么问题来了,这个静态方法interrupted()跟实例方法isInterrupted()有什么区别呢?

我们举个简单的例子对比下执行结果

静态方法interrupted()

执行结果如下:

实例方法isInterrupted()

执行结果如下

来看它俩的源码对比

可以看到其实它们调用的是同一个方法,只不过,在对于是否需要清理这个参数的传递上,静态方法传递的是true,而实例方法传递的是false,简单来讲就是多了一步“复位”操作。因此,才会出现上面两段示例代码执行的结果不一致,在当前线程未执行中断方法interrupt()时,当前线程的中断标识位本身就是初始值false,因此连续调用两次静态方法或者实例方法的执行结果都是false;当当前线程执行了中断方法interrupt()时,第一次调用静态方法和实例方法的返回值同样都是true,但是此时静态方法多做了一步“复位”操作,把当前线程的中断标识位重置回了初始值false,而实例方法则没有这步操作,因此,当第二次调用时,实例方法的示例中当前线程的中断标识位仍然是true,因此仍然返回true,而静态方法的示例代码中当前线程的中断标识位已经被重置回了false,于是就返回了false。

LockSupport是什么

LockSupport是java.util.concurrent.locks中的一个类,用于创建锁和其他同步类的基本线程阻塞原语

线程等待和唤醒机制

3种让线程等待唤醒的方法
  1. 使用Object中的wait()方法让线程等待,使用notify()方法唤醒线程
  2. 使用JUC包中的Condition的await()方法让线程等待,使用signal()方法唤醒线程
  3. 使用LockSupport类中的park()和unpark()方法阻塞当前线程以及唤醒指定被阻塞的线程
Object类中的wait()和notify()方法实现线程的等待和唤醒

示例代码:

public static void main(String[] args) {Object objectLock = new Object();new Thread(() -> {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}synchronized (objectLock) {System.out.println(Thread.currentThread().getName() + " 进入--->");try {objectLock.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 被唤醒^ ^");}}, "A").start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {synchronized (objectLock) {objectLock.notify();}System.out.println(Thread.currentThread().getName() + " 发出唤醒通知( ̄∇ ̄)/");}, "B").start();
}

执行结果如下:

问题:

  • 必须要先持有锁,否则会报错

  • 必须先wait()再notify(),否则会卡死

Condition接口中的await()后signal()方法实现线程的等待和唤醒

示例代码:

public static void main(String[] args) {Lock lock = new ReentrantLock();Condition condition = lock.newCondition();new Thread(() -> {try { TimeUnit.SECONDS.sleep(1); } catch ( InterruptedException e) { e.printStackTrace();}lock.lock();System.out.println(Thread.currentThread().getName() + " 进入--->");try {condition.await();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}System.out.println(Thread.currentThread().getName() + " 被唤醒^ ^");}, "A").start();try { TimeUnit.SECONDS.sleep(1); } catch ( InterruptedException e) { e.printStackTrace();}new Thread(() -> {lock.lock();try {condition.signal();} finally {lock.unlock();}System.out.println(Thread.currentThread().getName() + " 发出唤醒通知( ̄∇ ̄)/");}, "B").start();}

执行结果如下:

问题:

  • 必须要先持有锁,否则会报错

在lock、unlock对里面才能正确调用condition中线程等待和唤醒的方法。

  • 必须先await()再signal(),否则会卡死

上述两个对象Object和Condition使用的限制条件
  • 线程先要获得并持有锁,必须在锁块(synchronized/lock)中
  • 必须要先等待后唤醒,线程才能够被唤醒
LockSupport类中的park()等待和unpack()唤醒

LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每一个线程都有一个Permit(许可),但与Semaphore不同的是,许可的累加上限是1(最多一个许可)。

使用示例:

public static void main(String[] args) {Thread A = new Thread(() -> {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 进入--->");LockSupport.park();System.out.println(Thread.currentThread().getName() + " 被唤醒^ ^");}, "A");A.start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {LockSupport.unpark(A);System.out.println(Thread.currentThread().getName() + " 发出唤醒通知( ̄∇ ̄)/");}, "B").start();
}

执行结果如下:

可以看到,使用LockSupport进行线程阻塞/唤醒不需要在锁对块(synchronized/lock)中,所以上面Object类和Condition接口的第一个问题解决了,然后我们看看第二个问题:加锁/解锁的先后顺序。

如上图可以看出,park()和unpack()执行的先后顺序不会影响结果。

总结

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。

LockSupport是一个线程阻塞工具,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞后也有对应的唤醒方法,归根结底,LockSupport调用的是Unsafe中的native代码。

LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程,每个使用LockSupport的线程都一个Permit(许可)与LockSupport相关联,每一个线程都都一个相关的Permit,并且最多一个,重复调用unpark()也不会积累Permit。

换句话讲,线程阻塞需要消耗凭证,并且这个凭证最多1个

当调用park()方法时:

  • 如果有Permit,会消耗这个Permit,然后正常退出
  • 如果没有Permit,会阻塞当前线程等待获取凭证

当调用unpark()方法时:

  • 会增加一个Permit
  • 调用多次也只会有一个Permit(不累加)

最后,我们思考2个问题:

  1. 为什么LockSupport提供park()和unpark()方法可以突破Object和Condition的调用先后顺序的限制? 因为调用unpark()会获得1个Permit,之后再调用park()会消耗这个Permit,park()和unpark()方法的执行顺序不会影响线程的唤醒,即便先发放了Permit仍然不会阻塞线程。
  2. 唤醒两次(调用两次park()方法)后阻塞两次(调用两次unpark)方法),会发生什么情况? 线程会被阻塞,因为Permit不会累加,即使调用了两次unpark(),仍然只会有一个Permit,后面接着的调用两次park(),在调用第一次park()时,就会把这个Permit消耗掉,第二次调用时就没有Permit了,因而会被阻塞。

搞定( ̄∇ ̄)/🎉🎉🎉~~~~~~~~~~

这篇关于【JUC】Java并发编程从挖坑到入土全解(4-一文讲通LockSupport与线程中断->长图预警)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/182558

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

第10章 中断和动态时钟显示

第10章 中断和动态时钟显示 从本章开始,按照书籍的划分,第10章开始就进入保护模式(Protected Mode)部分了,感觉从这里开始难度突然就增加了。 书中介绍了为什么有中断(Interrupt)的设计,中断的几种方式:外部硬件中断、内部中断和软中断。通过中断做了一个会走的时钟和屏幕上输入字符的程序。 我自己理解中断的一些作用: 为了更好的利用处理器的性能。协同快速和慢速设备一起工作

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟 开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚 第一站:海量资源,应有尽有 走进“智听