Java 并发编程归纳总结(可重入锁 | JMM | synchronized 实现原理)

1、锁的可重入

一个不可重入的锁,抢占该锁的方法递归调用自己,或者两个持有该锁的方法之间发生调用,都会发生死锁。以之前实现的显式独占锁为例,在递归调用时会发生死锁:

public class MyLock implements Lock {/* 仅需要将操作代理到Sync上即可*/private final Sync sync = new Sync();private final static class Sync extends AbstractQueuedSynchronizer {// 判断处于独占状态@Overrideprotected boolean isHeldExclusively() {return getState() == 1;}// 获得锁@Overrideprotected boolean tryAcquire(int i) {if (compareAndSetState(0, 1)) {// 设置占有独占锁的线程setExclusiveOwnerThread(Thread.currentThread());return true;}return false;}// 释放锁@Overrideprotected boolean tryRelease(int i) {if (getState() == 0) {throw new IllegalMonitorStateException();}setExclusiveOwnerThread(null);setState(0);return true;}// 返回一个Condition,每个condition都包含了一个condition队列public Condition newCondition() {return new ConditionObject();}}@Overridepublic void lock() {System.out.println(Thread.currentThread().getName() + " ready get lock");sync.acquire(1);System.out.println(Thread.currentThread().getName() + " already got lock");}@Overridepublic void lockInterruptibly() throws InterruptedException {sync.acquireInterruptibly(1);}@Overridepublic boolean tryLock() {return sync.tryAcquire(1);}@Overridepublic boolean tryLock(long timeout, TimeUnit timeUnit) throws InterruptedException {return sync.tryAcquireNanos(1, timeUnit.toNanos(timeout));}@Overridepublic void unlock() {System.out.println(Thread.currentThread().getName() + " ready release lock");sync.release(1);System.out.println(Thread.currentThread().getName() + " already released lock");}@Overridepublic Condition newCondition() {return sync.newCondition();}
}

测试代码:

public class Test {private static MyLock lock = new MyLock();private static class TestThread implements Runnable {public TestThread() {}@Overridepublic void run() {System.out.println(Thread.currentThread().getName());try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}reenter(3);}public void reenter(int level) {lock.lock();try {System.out.println(Thread.currentThread().getName() + ":递归层级:" + level);if (level == 0) return;reenter(level - 1);} finally {lock.unlock();}}}public static void main(String[] args) {for (int i = 0; i < 3; i++) {Thread thread = new Thread(new TestThread(new JavaBean(0)));thread.start();}}
}

输出结果:

Thread-2 ready get lock
Thread-0 ready get lock
Thread-2 already got lock
Thread-1 ready get lock
Thread-2:递归层级:3
Thread-2 ready get lock

代码停在这里发生死锁,原因是 Thread-2 已经拿到了锁,在递归到下一层时,还要获取 lock,但是 MyLock 没实现可重入,使得它在执行 tryAcquire() 的原子操作 compareAndSetState(0,1) 时一直不成功,因为期望值此时已经由 0 变成了 1。所以这里需要实现可重入锁。

想要实现可重入的锁,需要让 state 作为锁的计数器:

        // 获得锁@Overrideprotected boolean tryAcquire(int i) {if (compareAndSetState(0, 1)) {setExclusiveOwnerThread(Thread.currentThread());return true;} else if (getExclusiveOwnerThread() == Thread.currentThread()) {setState(getState() + 1);return true;}return false;}// 释放锁@Overrideprotected boolean tryRelease(int i) {if (getExclusiveOwnerThread() != Thread.currentThread()) {throw new IllegalMonitorStateException();}if (getState() == 0) {throw new IllegalMonitorStateException();}setState(getState() - 1);if (getState() == 0) {setExclusiveOwnerThread(null);}return true;}

state 作为持有这个锁的线程的数量,锁被持有了几次,就要相应的释放几次。

2、Java 内存模型(JMM)

Java 内存模型的图示

上图中工作内存和主内存是两个抽象的概念,不是真实存在的实体,它们可以是 CPU 寄存器、CPU 中的高速缓存,甚至是主内存 RAM 的一部分。

线程在执行计算工作时,会把需要用到的变量从主内存拷贝到自己的工作内存中。线程不能直接操作主内存中的数据,也不能访问其它线程工作内存。这样的内存模型使得线程执行过程中面临两个问题:可见性与原子性。

2.1 可见性与原子性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量。对于共享变量 V,多个线程先是在自己的工作内存,之后再同步到主内存。但同步动作并不会及时的刷到主存中,而是会有一定时间差。这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了。

要解决共享对象可见性这个问题,可以使 用volatile 关键字或者是加锁。

原子性即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

CPU 资源的分配都是以线程为单位的。任务切换大多数是在时间片段结束以后,当然也可以在任何一条 CPU 指令执行完之后(是 CPU 指令而不是某种高级语言的一个语句,如 Java 中的 count++ 至少需要三条 CPU 指令才能完成),这也可能导致线程安全问题。

举个例子,假如两个线程都执行语句 count = count + 1,如图所示:

多线程工作内存示例

线程 A、B 都把 count 的初值 0 从主内存拷贝到自己的工作内存中开始执行 count + 1 的操作,都得到结果 1 再把副本值同步到主内存中。明明进行了两次计算,但是得到的却是计算了一次的结果,这是因为两个线程对于 count 的操作是互不可见的,彼此不知道对 count 的操作。

上述问题发生的原因是未能保证线程操作的可见性,可以使用 volatile 关键字或者是加锁解决可见性问题

但是使用 volatile 修饰 count 后问题仍没有解决,原因就是 count = count + 1 并不是一个原子操作,完全有可能在执行完 count + 1 之后,赋值给 count 之前,CPU 进行上下文切换到其它线程执行完整个 count = count + 1 并将结果同步回内存,最后切换会原线程继续执行的情况,这就是原子性问题。

2.2 volatile 关键字

volatile 是 Java 并发编程包中最轻量级的一个同步工具。

使用 volatile 关键字修饰一个变量,会强迫线程每次在计算该变量之前从主内存中拿最新的变量值,并且要求计算完成后立即将新的变量值同步到主内存中。

可以把对 volatile 变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。如:

public class Volatile {volatile int i;// 单个 volatile 变量的读public int getI() {return i;}// 单个 volatile 变量的写public void setI(int i) {this.i = i;}private void inc() {// Non-atomic operation on volatile field 'i'i++; // 复合(多个)volatile 变量的读/写}
}

等价于:

public class Volatile {int i;// 单个 volatile 变量的读public synchronized int getI() {return i;}// 单个 volatile 变量的写public synchronized void setI(int i) {this.i = i;}private void inc() {// 调用同步读int temp = getI(); // 普通写,可能在执行这一步之前发生线程切换导致 volatile 修饰的变量发生线程安全问题temp = temp + 1;// 调用同步写setI(temp);}
}

可见 volatile 只能保证对变量的单个操作的线程安全,但像 i++ 这种复合操作,volatile 则不能保证其线程安全。

因此 volatile 变量自身具有以下特性:

  • 可见性:对一个 volatile 变量的读,总是能看到任意线程对这个 volatile 变量最后的写入。
  • 原子性:对任意单个 volatile 变量的读/写具有原子性,但类似于 i++ 这种复合操作不具有原子性。

volatile 适用的场景有:

  1. 一个线程写,多个线程读。(写线程能立即将结果写回内存,而读线程能拿到变量在内存中最新的值。否则写线程的结果可能并不是立即写回内存的,导致读线程拿到的变量不是最新)
  2. 多个线程写,但是各个线程写的值没有关联(count = 5 这种直接赋值是没有关联的,但是像 count = count +1 这种基于 count 原始值的认为是有关联)。

volatile 还有一个功能就是抑制重排序

重排序是指在现代 CPU 中同一时刻可以执行多条指令,可能会造成实际执行的代码顺序与编写的顺序不同的情况。例如:

    do(...) {int a = 5;  // 1int b = 10; // 2int t;if (b == 5) {t = b;}}

指令重排后可能后编写的语句会先被执行,即便 b == 5 的条件还未满足,也先执行 t = b,只不过对于这种条件语句可能会先存入重排序缓冲区中,等到 b == 5 满足时再从缓冲中取出执行。重排序在单线程中是不会出现乱序问题的,但是多线程则可能会出现。如果用 volatile 修饰某个变量,就不会对其进行重排序。

Intel CPU 可以有十级流水线(即 CPU 可以在同一时刻执行十条指令),Android 芯片的 ARM 架构也可达到三级流水线。

volatile 的实现原理是被 volatile 修饰的共享变量进行读写操作的时候会使用 CPU 提供的 Lock 前缀指令。该指令的作用是:

  • 将当前处理器缓存行的数据写回到系统内存。
  • 写回内存的操作会使其它 CPU 里缓存了该内存地址的数据无效。

以上是对 volatile 关键字的介绍。最后我们再回头看下 count = count + 1 的问题的解决方案:

  1. 使用 volatile 关键字搭配 CAS 操作,前者保证可见性,后者保证原子性。实际上 JDK 中很多同步操作都是使用 volatile + CAS 来代替 synchronized
  2. 直接用锁,synchronized、Lock…

3、synchronized 实现原理

3.1 monitorenter 和 monitorexit 指令

底层是使用 monitorenter 和 monitorexit 指令实现的。对于使用了 synchronized 同步代码块的代码:

public class IncTest {private int count;// public 才能被 javap 反编译出来public int inc() {synchronized (this) {return count++;}}
}

编译后使用 javap -v IncTest.class 命令反编译,会看到 inc 方法的汇编指令:

同步代码块的汇编指令

真正执行 count++ 操作的指令是在第4行~第15行,而第3行的 monitorenter 与第16行的 monitorexit 则分别是获取锁和释放锁的指令。这两条指令是由编译器插入的。而使用同步方法时:

public class IncTest {private int count;// public 才能被 javap 反编译出来public synchronized int inc() {return count++;}
}

其汇编指令为:

同步方法汇编指令

同步方法的汇编指令没有显式的 monitorenter 和 monitorexit 指令,但是在方法的 flags 上能看到多出了一个 ACC_SYNCHRONIZED,在运行时还是用到了 monitorenter 和 monitorexit 指令,只不过无法在字节码指令上体现出来。

总结一下:

  • monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处。
  • 每个 monitorenter 必须有对应的 monitorexit 与之配对。
  • 任何对象都有一个 monitor 与之关联。

3.2 锁的存放位置与锁升级

Java 对象在内存中由三部分组成:对象头、实例数据和对齐填充字节。synchronized 锁就存放在对象头中,它由三部分组成:Mark Word、指向类的指针(也称 KlassPoint)和数组长度(只有数组对象才有):

对象头的组成部分

它们的长度与虚拟机位数保持一致,以 32 位为例,Mark Word 的存储内容是这样的:

分代年龄是指对象经历过 GC 的次数。堆内存至少会被划分成两部分,一部分存放新生代对象,一部分存放老年代对象。JVM 默认一个新生代对象经历过15次 GC 还没有被回收,就认为该对象是一个需要长期储存的对象,于是就把它移入堆内存的老年代存放区。

我们都知道 synchronized 同步锁是一个重量级的锁,拿锁失败的线程会发生上下文切换被阻塞,直到拿到锁后又发生上下文切换由阻塞状态变成运行状态。因为上下文切换的耗时相对于 CPU 执行指令的时间是非常耗时的,一次上下文切换需要大概5000~20000个单位时间,在3~5毫秒左右,而一个1.6G的 CPU 执行一条指令耗时0.6纳秒,对于一个100条指令的任务,CPU 的执行时间也就仅仅在0.6毫秒左右。因此,如果使用 synchronized 执行一个较轻量级的任务,被阻塞等待的时间远远超过了执行任务本身所需的时间。

为了对上述情况做出优化,从 JDK 1.6 开始出现了锁升级的概念,意思是说,一个 synchronized 锁在 Mark Word 中的状态不是一成不变的,会根据任务的量级对锁的量级逐步提升,即无锁状态->偏向锁状态->轻量级锁状态->重量级锁状态(锁的四种状态):

3.2.1 偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得(统计发现),为了让线程获得锁的代价更低而引入了偏向锁(锁总是会倾向于分配给第一次拿到这个锁的线程)。无竞争时不需要进行 CAS 操作来加锁和解锁,而是直接把锁给到当前线程。但是一旦发生多个线程间的资源竞争,就要把偏向锁升级为轻量级锁,在升级之前,要先撤销偏向锁。

偏向锁执行流程

偏向锁撤销时中有一个 Stop the World 现象。Stop the World 是指:

在新生代进行的GC叫做minor GC,在老年代进行的GC都叫major GC,Full GC同时作用于新生代和老年代。在垃圾回收过程中经常涉及到对对象的挪动(比如上文提到的对象在Survivor 0和Survivor 1之间的复制),进而导致需要对对象引用进行更新。为了保证引用更新的正确性,Java将暂停所有其他的线程,这种情况被称为“Stop-The-World”,导致系统全局停顿。Stop-The-World对系统性能存在影响,因此垃圾回收的一个原则是尽量减少“Stop-The-World”的时间。
引用自 JVM学习(7)Stop-The-World

看上图,线程2在撤销线程1的偏向锁时,需要修改线程1工作内存中的相关数据,在修改之前要停止线程1的执行,否则线程2无法修改。因此这里也是发生了 Stop the World 现象,由于它会停止其它线程,因此在有多个线程竞争资源时,是不推荐使用偏向锁的。

3.2.2 轻量级锁

轻量级锁通过 CAS 操作来加锁和解锁。其中的自旋锁借鉴了 CAS 的思想,不会阻塞没有拿到锁的线程,而是让其自旋。假如获取到锁的那个线程执行速度很快,那么自旋中的线程也可能很快就拿到了锁,这样能节省出两次上下文切换的时间。

但是自旋是占用 CPU 在不停的循环执行检测的,倘若线程任务中有访问服务器之类的重量级操作,如果还是一直不停的自旋,就使得 CPU 不能充分的利用。因此又产生了适应性自旋锁,它会根据算法决定自旋的时间/次数,一般这个时间就是一次上下文切换的时间。因为引入轻量级锁的目的就是通过自旋节省掉使用重量级锁时产生的上下文切换的时间,如果自旋时间已经超过上下文切换时间,那么自旋也就没有意义了,此时就要把轻量级锁膨胀为重量级锁。

轻量级锁执行流程

不同重量级锁的比较

锁只能升级,不能降级。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/905854.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据治理域——数据同步设计

摘要 本文主要介绍了数据同步的多种方式&#xff0c;包括直连同步、数据文件同步和数据库日志解析同步。每种方式都有其适用场景、技术特点、优缺点以及适用的数据类型和实时性要求。文章还详细探讨了数据直连同步的特点、工作原理、优点、缺点、适用场景等&#xff0c;并对数…

AI人工智能在教育领域的应用

AI人工智能在教育领域的应用 随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;逐渐成为推动教育变革的重要力量。AI在教育领域的应用不仅改变了传统的教学模式&#xff0c;还为个性化学习、教育资源优化和教育管理带来了前所未有的机遇。本文将从多个方面探…

ohttps开启群晖ssl证书自动更新

开启群晖ssl证书自动更新OHTTPS ohttps是一个免费自动签发ssl证书、管理、部署的项目。 https://ohttps.com 本文举例以ohttps项目自动部署、更新群晖的ssl证书。 部署 签发证书 打开ohttps-证书管理-创建证书-按你实际情况创建证书。创建部署节点 打开Ohttps-部署节点-添加…

ElasticSearch聚合操作案例

1、根据color分组统计销售数量 只执行聚合分组&#xff0c;不做复杂的聚合统计。在ES中最基础的聚合为terms&#xff0c;相当于 SQL中的count。 在ES中默认为分组数据做排序&#xff0c;使用的是doc_count数据执行降序排列。可以使用 _key元数据&#xff0c;根据分组后的字段数…

SQLite 数据库常见问题及解决方法

一、数据库文件锁定问题 1. 问题表现 在多线程或多进程环境下访问 SQLite 数据库时&#xff0c;常常会出现数据库文件被锁定的情况。当一个进程对数据库执行写操作时&#xff0c;其他进程的读写操作都会被阻塞&#xff0c;导致应用程序出现卡顿甚至无响应。比如在移动应用开发…

DeepSeek基础:PPO、DPO、GRPO概念详解

DeepSeek-R1 的强化学习方案中&#xff0c;其亮点之一在于通过 GRPO 算法取代RLHF 常用的 PPO&#xff0c;通过尽可能减少人类标注数据&#xff0c;设计纯 RL 的环境&#xff0c;用精心设计的奖励机制来训练模型自己学会推理。那么什么是PPO、GRPO&#xff0c;其产生的背景、核…

一分钟了解机器学习

一分钟了解机器学习 A Minute to Know About Machine Learning By JacksonML 1. 什么是机器学习&#xff1f; 机器学习&#xff08;Machine Learning,ML&#xff09; 是人工智能的分支&#xff0c;通过从数据中自动学习规律&#xff0c;使计算机无需显式编程即可完成任务。…

mvc-service引入

什么是业务层 1&#xff09;Model1&#xff08;JSP&#xff09;和Model2&#xff08;模糊的mvc&#xff09;: MVC&#xff1a;Model(模型)&#xff0c;View(视图)&#xff0c;Controller&#xff08;控制器&#xff09; 视图层&#xff1a;用于数据展示以及用户交互的界…

第一次做逆向

题目来源&#xff1a;ctf.show 1、下载附件&#xff0c;发现一个exe和一个txt文件 看看病毒加没加壳&#xff0c;发现没加那就直接放IDA 放到IDA找到main主函数&#xff0c;按F5反编译工具就把他还原成类似C语言的代码 然后我们看逻辑&#xff0c;将flag.txt文件的内容进行加…

docker(四)使用篇二:docker 镜像

在上一章中&#xff0c;我们介绍了 docker 镜像仓库&#xff0c;本文就来介绍 docker 镜像。 一、什么是镜像 docker 镜像本质上是一个 read-only 只读文件&#xff0c; 这个文件包含了文件系统、源码、库文件、依赖、工具等一些运行 application 所必须的文件。 我们可以把…

k8s 1.10.26 一次containerd失败引发kubectl不可用问题

k8s 1.10.26 一次containerd失败引发kubectl不可用问题 开机k8s 1.10.26时&#xff0c;报以下错误 [rootmaster ~]# kubectl get no E0515 08:03:00.914894 7993 memcache.go:265] couldnt get current server API group list: Get "https://192.168.80.50:6443/api?…

今日积累:若依框架配置QQ邮箱,来发邮件,注册账号使用

QQ邮箱SMTP服务器设置 首先&#xff0c;我们需要了解QQ邮箱的SMTP服务器地址。对于QQ邮箱&#xff0c;SMTP服务器地址通常是smtp.qq.com。这个地址适用于所有使用QQ邮箱发送邮件的客户端。 QQ邮箱SMTP端口设置 QQ邮箱提供了两种加密方式&#xff1a;SSL和STARTTLS。根据您选…

无缝部署您的应用程序:将 Jenkins Pipelines 与 ArgoCD 集成

在 DevOps 领域&#xff0c;自动化是主要目标之一。这包括自动化软件部署方式。与其依赖某人在部署软件的机器上进行 rsync/FTP/编写软件&#xff0c;不如使用 CI/CD 的概念。 CI&#xff0c;即持续集成&#xff0c;是通过代码提交创建工件的步骤。这可以是 Docker 镜像&#…

4.2.3 Thymeleaf标准表达式 - 5. 片段表达式

在本次实战中&#xff0c;我们通过 Thymeleaf 的片段表达式实现了模板的模块化和复用。首先&#xff0c;我们定义了一个导航栏片段 navbar&#xff0c;并通过参数 activeTab 动态高亮当前激活的标签。然后&#xff0c;我们在多个页面&#xff08;如主页、关于页和联系页&#x…

网安面试经(1)

1.说说IPsec VPN 答&#xff1a;IPsec VPN是利用IPsec协议构建的安全虚拟网络。它通过加密技术&#xff0c;在公共网络中创建加密隧道&#xff0c;确保数据传输的保密性、完整性和真实性。常用于企业分支互联和远程办公&#xff0c;能有效防范数据泄露与篡改&#xff0c;但部署…

【C++/Qt shared_ptr 与 线程池】合作使用案例

以下是一个结合 std::shared_ptr 和 Qt 线程池&#xff08;QThreadPool&#xff09;的完整案例&#xff0c;展示了如何在多线程任务中安全管理资源&#xff0c;避免内存泄漏。 案例场景 任务目标&#xff1a;在后台线程中处理一个耗时的图像检测任务&#xff0c;任务对象通过 …

【Unity】 HTFramework框架(六十五)ScrollList滚动数据列表

更新日期&#xff1a;2025年5月16日。 Github 仓库&#xff1a;https://github.com/SaiTingHu/HTFramework Gitee 仓库&#xff1a;https://gitee.com/SaiTingHu/HTFramework 索引 一、ScrollList滚动数据列表二、使用ScrollList1.快捷创建ScrollList2.ScrollList的属性3.自定义…

经典案例 | 筑基与跃升:解码制造企业产供销协同难题

引言 制造企业如何在投产初期突破管理瓶颈&#xff0c;实现高效运营&#xff1f;G公司作为某大型集团的新建子公司&#xff0c;面对产供销流程缺失、跨部门协同低效等难题&#xff0c;选择与AMT企源合作开展流程优化。 项目通过端到端流程体系搭建、标准化操作规范制定及长效管…

【Python 操作 MySQL 数据库】

在 Python 中操作 MySQL 数据库主要通过 pymysql 或 mysql-connector-python 库实现。以下是完整的技术指南&#xff0c;包含连接管理、CRUD 操作和最佳实践&#xff1a; 一、环境准备 1. 安装驱动库 pip install pymysql # 推荐&#xff08;纯Python实现&#xff0…

记录vsCode连接gitee并实现项目拉取和上传

标题 在 VSCode 中上传代码到 Gitee 仓库 要在 VSCode 中将代码上传到 Gitee (码云) 仓库&#xff0c;你可以按照以下步骤操作&#xff1a; 准备工作 确保已安装 Git确保已安装 VSCode拥有 Gitee 账号并创建了仓库 可以参考该文章的部分&#xff1a;idea实现与gitee连接 操…