Java多线程锁技术漫谈:乐观锁VS悲观锁

news/2024/12/2 17:02:07/
 **Java多线程技术一直是Java程序员必备的核心技能之一。在Java多线程编程中,为了保证数据的一致性和安全性,常常需要使用锁的机制来防止多个线程同时修改同一个共享资源。锁是实现并发访问控制的一种机制,多线程之间共同访问共享资源的时候,悲观锁是最常见的方式,不过在高并发场景中,全局锁所带来的并发性阻塞问题也是不可避免的。为了解决这种问题,人们又引出了乐观锁的概念。在本文中,我们将详细探讨Java多线程中的乐观锁和悲观锁。**

一、什么是悲观锁

悲观锁是多线程并发控制的一种机制。悲观锁一般都是基于数据库锁的实现方式实现的。当一个线程要对共享资源进行访问时,那么就会使用悲观锁定义好的机制获取该资源的锁。这个过程中,如果其他线程也想获取该锁,就需要等待当前线程释放锁之后才能获取该锁。当然,也可以通过设置超时时间等机制来避免死锁等问题。

在Java中,我们可以使用synchronized关键字实现悲观锁,synchronized可以在对象级别上使用,也可以在类级别上使用。当某个线程访问synchronized锁定的对象时,该对象的状态会被设置为被锁定状态,如果其他线程也想修改这个状态,就必须等待当前线程释放锁之后才能获取该锁。这种方式看似非常理想,但实际上只是针对小并发量的业务场景。

public class LockExample {private int count = 0;public synchronized void increment() {// some code herethis.count++;}
}

在上面的代码中,使用了synchronized关键字来实现悲观锁。在increment()方法中,使用了this关键字来对整个方法进行加锁,确保在一个线程执行到该方法期间,不会有其他线程同时访问这个方法。

悲观锁的缺点是,由于每个线程在访问资源之前都需要获取锁,因此当并发性非常高的时候,会导致大量的线程在等待锁,从而让整个应用程序的性能急剧下降。
在高并发量的业务场景中,使用悲观锁就显得不太合适了,它很容易造成死锁等问题,从而导致应用程序的性能下降。

二、什么是乐观锁

如果说悲观锁是一种防守性的锁,那么乐观锁就是一种进攻性的锁,它尝试着最大程度地避免锁定资源。乐观锁更多地体现了一种“乐观”的思想,它认为多个线程访问相同的资源的概率并不是那么大,所以在访问共享资源的时候,它并不对该资源进行特别的锁定操作,而是直接针对该资源执行读取和修改操作,并在修改操作完成之后进行版本校验。如果读取到的版本号与当前版本号一致,表示操作成功,程序继续运行,否则表示已经有其他线程对该资源进行了修改,则需要进行Retry操作,重新读取版本号,然后再进行更新操作。

在Java当中,乐观锁的实现方式主要有两种,一种是CAS,即Compare And Swap,另一种是版本号机制(例如AtomicInteger类)。

使用Compare-And-Swap算法

CAS是一种通过让CPU底层内存操作指令实现原子操作的机制。CAS机制操作的原理是将当前内存中的值与CAS指令中的值进行比较,如果一致,则将当前内存中的值更新为新的值,如果不一致,则重新执行该操作。
Compare-And-Swap(CAS)算法是一种基于乐观锁的算法,能够实现非常高的并发性。基本思路是,在我们修改共享资源的时候,先读取这个资源当前的状态,然后对这个状态进行比对,如果状态没有发生改变,就更新这个资源的状态,否则忽略这次修改操作,并等待下一次机会再去修改。

下面是一个使用CAS算法来实现乐观锁的示例:

public class CASExample {private volatile int value;public void increment() {while (true) {// 使用CAS算法进行操作int current = this.value;int next = current + 1;if (compareAndSwap(current, next)) {break;}}}// 比较并替换方法private synchronized boolean compareAndSwap(int current, int next) {if (this.value == current) {this.value = next;return true;}return false;}
}

在该例子中,increment()方法不断循环获取共享资源的值,然后判断是否需要更新资源。当需要更新资源的时候,它会调用compareAndSwap()方法来进行比较并替换操作。如果当前的值与我们读取的值相同,则将新的值替换掉旧的值。

需要注意的是,在Compare-And-Swap算法中,只有当共享资源足够热门且争用激烈的时候,CAS算法才能发挥真正的作用,否则,因为CAS算法需要额外的操作来获取当前资源的状态,因此它的性能可能比传统的悲观锁机制还要低。

使用版本号机制AtomicInteger类

版本号机制,也称为时间戳机制,在执行Redis等缓存操作时比较常见。其核心思想是在每个需要被控制的资源对象中增加一个版本号字段,在每次执行修改操作的时候,都需要对该版本号进行更新。如果修改成功,则版本号 +1,否则不做任何操作。这样,在执行读取该资源的操作时,只需要比对版本号即可,如果版本号一致,则表示可以进行后续操作,否则表示已经有其他线程对该资源进行了修改,需要进行Retry操作。
AtomicInteger类是线程安全的,可以轻松的实现乐观锁。在下面的代码中,我们使用AtomicInteger来实现一个计数器,使用incrementAndGet()方法来实现自增操作。

public class AtomicIntegerExample {private AtomicInteger count = new AtomicInteger();public void increment() {// some code herethis.count.incrementAndGet();}
}

三、悲观锁和乐观锁的比较

在实际开发中,悲观锁和乐观锁都有各自的优缺点,开发人员需要根据具体的业务场景灵活选择。下面,我们将对悲观锁和乐观锁进行详细比较。

3.1 实现难度

悲观锁的实现相对来说还是比较简单的,只需要在代码中引入synchronized关键字等机制即可。而乐观锁的实现难度就要大一些了。在使用CAS机制时,需要使用一些较为底层的技术,整个实现过程比较繁琐,需要小心处理其边界条件的问题,尤其是针对高并发场景的时候。

3.2 性能表现

在高并发场景下,乐观锁的性能表现往往明显优于悲观锁。这是由于悲观锁对共享资源进行频繁的锁定和解锁操作,很容易引起线程阻塞,从而导致系统的性能下降。因此,在高并发场景下,采用乐观锁能够更好地提高程序的并发性能。

3.3 实现复杂度

悲观锁实现本质上就是一个加锁/解锁的过程。而乐观锁则需要涉及到版本控制等方面。因此,实现复杂度方面,悲观锁要明显低于乐观锁。

3.4 数据冲突的处理

悲观锁能够很好地处理数据冲突问题,因为它始终锁定共享资源,排除其他线程的干扰,从而有效避免数据冲突问题。而乐观锁则需要对版本号进行控制,如果版本号不一致,则需要对该资源进行Retry操作,如果Retry次数过多,那么就可能会引发重试超时等问题,从而影响系统的正常运行。

四、总结

从上述比较的内容中我们可以看出,悲观锁和乐观锁各自有一些优缺点,在实际开发中,开发人员需要根据具体的业务需求、访问频率等因素进行灵活选择。悲观锁在并发性能较低的情况下,可以保证数据的一致性,但是因为每个线程在访问资源时都需要获取锁,因此它的性能可能不是很好。相比之下,乐观锁能够利用CAS算法等技术来保证资源的一致性,从而实现更高的并发性能。在高并发场景中,乐观锁能够更好地提高程序的并发性能,不过需要注意对版本号进行控制,避免重试次数过多的情况发生。无论哪种锁机制,都需要注意保证数据的一致性和安全性,避免发生脏读、幻读、乱序写等不安全的情况。

附加代码解析

完整的代码如下:

import java.util.concurrent.atomic.AtomicInteger;public class LockExample {private int count1 = 0;private AtomicInteger count2 = new AtomicInteger();private volatile int count3;public synchronized void increment1() {this.count1++;}public void increment2() {this.count2.incrementAndGet();}public void increment3() {while (true) {int current = this.count3;int next = current + 1;if (compareAndSwap(current, next)) {break;}}}private synchronized boolean compareAndSwap(int current, int next) {if (this.count3 == current) {this.count3 = next;return true;}return false;}
}

在该代码中,我们定义了一个LockExample类,包含了三个属性count1、count2、count3,分别用于演示悲观锁、AtomicInteger乐观锁和CAS算法的乐观锁。这些属性都实现了一个increment()方法,用于对属性进行自增操作。这里,我们使用了三种不同的锁机制,分别是使用synchronized关键字的悲观锁、使用AtomicInteger类的乐观锁,以及使用CAS算法的乐观锁。通过上面的代码,我们可以更好的了解Java多线程中的锁机制。


http://www.ppmy.cn/news/122353.html

相关文章

如何使用Vue CLI来创建和管理Vue项目

Vue CLI是Vue.js官方提供的脚手架工具,它可以快速轻松地创建、配置和管理Vue项目。接下来,让我们了解一下如何使用Vue CLI吧! 首先,你需要确保已经安装了Node.js和npm。如果没有,请先访问https://nodejs.org/下载并安…

广通优云徐育毅:筑基数字化,打造中国IT运维新范式

2023年2月,中共中央、国务院印发《数字中国建设整体布局规划》,数字技术与经济、政治、文化、社会等各领域融合愈发紧密,一册百花齐放、生机勃勃的数字化图卷正徐徐展开。 随着数字中国战略深入推进,IT运维作为核心领域受到关注。…

真的被00后卷麻了,还好我很会划水~

前情提要 鉴于目前测试就业越来越严峻,内卷也成了测试领域的代名词了。我的一个HR朋友告诉我,现在测试岗的投递比已经将近1000,也就是一个岗位差不多有千份简历投进来,实在是恐怖! 前段时间我也去面试了一些公司&…

【Python笔记(三)——时间模块 函数 模块与包 面向对象 文件基本操作】

文章目录 Python 基础:与时间相关的模块获取当前时间格式化时间获取时间差 Python 基础:函数Python 基础:模块与包Python 基础:面向对象Python 基础:文件基本操作 Python 基础:与时间相关的模块 Python 中…

今天给码农们分享五个只有程序猿才能听懂的笑话

今天给码农们分享五个只有程序猿才能听懂的笑话 1.“神舟十一号,地面信号异常,现在请汇报您的具体位置?”“现在我们正在祖国上空”“你们怎么知道的?”“刚才试验了一下,Twitter和FaceBook都打不开。”2、程序员换ID…

达美乐的面试(部分)(未完全解析)

Java如何保证非线程安全的数据结构(比如HashMap)的原子性?读多写少时用哪种锁好? A: 方法1:CAS等乐观锁机制,方法2:如果读多写少,可以使用读写锁(ReentrantReadWriteLock&#xff0…

黑鹰安全网_育鹰计划_笔记

之前学习黑鹰基地网赚教程整理的笔记。 育鹰计划第一课 幼儿园小班 1,互联网是一次从未有过的商机,作为21世界的我们要好好把握. 2,网络的可复制性可以大大降低网赚的成本 3,放的下才能成长(不放下共享你已有的旧的东西;就不能进步,无法…

《疯狂的程序员》四

31 听周总这么说,绝影吓了一跳,不光他吓了一跳,BOSS Liu和 张厂长也吓了一跳。在他们印象中,周总脸上总是带着平静地表情,即使偶尔批评起人来,也还是带着平静地表情。这时候,绝影突然想起念大学…