Java中锁的解决方案

前言

在上一篇文章中,介绍了什么是锁,以及锁的使用场景,本文继续给大家继续做深入的介绍,介绍JAVA为我们提供的不同种类的锁。

JAVA为我们提供了种类丰富的锁,每种锁都有不同的特性,锁的使用场景也各不相同。由于篇幅有限,在这里只给大家介绍比较常用的几种锁。我会通过锁的定义,核心代码剖析,以及使用场景来给大家介绍JAVA中主流的几种锁。

乐观锁 与 悲观锁

乐观锁与悲观锁应该是每个开发人员最先接触的两种锁。小编最早接触的就是这两种锁,但是不是在JAVA中接触的,而是在数据库当中。当时的应用场景主要是在更新数据的时候,更新数据这个场景也是使用锁的非常主要的场景之一。更新数据的主要流程如下:

  1. 检索出要更新的数据,供操作人员查看;
  2. 操作人员更改需要修改的数值;
  3. 点击保存,更新数据;

这个流程看似简单,但是我们用多线程的思维去考虑,这也应该算是一种互联网思维吧,就会发现其中隐藏着问题。我们具体看一下

  1. A检索出数据;
  2. B检索出数据;
  3. B修改了数据;
  4. A修改数据,系统会修改成功吗?

当然啦,A修改成功与否,要看程序怎么写。咱们抛开程序,从常理考虑,A保存数据的时候,系统要给提示,说“您修改的数据已被其他人修改过,请重新查询确认”。那么我们程序中怎么实现呢?

  1. 在检索数据,将数据的版本号(version)或者最后更新时间一并检索出来;
  2. 操作员更改数据以后,点击保存,在数据库执行update操作
  3. 执行update操作时,用步骤1检索出的版本号或者最后更新时间与数据库中的记录作比较;
  4. 如果版本号或最后更新时间一致,则可以更新;
  5. 如果不一致,就要给出上面的提示;

上述的流程就是乐观锁的实现方式。在JAVA中乐观锁并没有确定的方法,或者关键字,它只是一个处理的流程、策略。咱们看懂上面的例子之后,再来看看JAVA中乐观锁。

乐观锁呢,它是假设一个线程在取数据的时候不会被其他线程更改数据,就像上面的例子那样,但是在更新数据的时候会校验数据有没有被修改过。它是一种比较交换的机制,简称CAS (Compare And Swap)机制。一旦检测到有冲突产生,也就是上面说到的版本号或者最后更新时间不一致,它就会进行重试,直到没有冲突为止。乐观锁的机制如图所示:

咱们看一下JAVA中最常用的i++,咱们思考一个问题,i++它的执行顺序是什么样子的?它是线程安全的吗?当多个线程并发执行i++的时候,会不会有问题?接下来咱们通过程序看一下:

package cn.pottercoding.lock;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** @author 程序员波特* @since 2024年01月12日** i++ 线程安全问题测试*/
public class ThreadTest {private int i = 0;public static void main(String[] args) {ThreadTest test = new ThreadTest();// 线程池,50个固定线程ExecutorService executorService = Executors.newFixedThreadPool(50);CountDownLatch countDownLatch = new CountDownLatch(5000);for (int i = 0; i < 5000; i++) {executorService.execute(() -> {test.i++;countDownLatch.countDown();});}executorService.shutdown();try {countDownLatch.await();System.out.println("执行完成后,i = " + test.i);} catch (InterruptedException e) {e.printStackTrace();}}
}

上面的程序中,我们模拟了50个线程同时执行i++,总共执行5000次,按照常规的理解,得到的结果应该是5000,我们运行一下程序,看看执行的结果如何?

执行完成后,i=4975

执行完成后,i=4986

执行完成后,i=4971

这是运行3次以后得到的结果,可以看到每次执行的结果都不一样,而且不是5000,这是为什么呢?这就说明i++并不是一个原子性的操作,在多线程的情况下并不安全。我们把i++的详细执行步骤拆解一下:

  1. 从内存中取出i的当前值;
  2. 将i的值加1;
  3. 将计算好的值放入到内存当中;

这个流程和我们上面讲解的数据库的操作流程是一样的。在多线程的场景下,我们可以想象一下,线程A和线程B同时从内存取出的值,假如i的值是1000,然后线程A和线程B再同时执行+1的操作,然后把值再放入内存当中,这时,内存中的值是1001,而我们期望的是1002,正是这个原因导致了上面的错误。那么我们如何解决呢?在JAVA1.5以后,JDK官方提供了大量的原子类,这些类的内部都是基于CAS机制的,也就是使用了乐观锁。我们将上面的程序稍微改造一下,如下:

package cn.pottercoding.lock;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;/*** @author 程序员波特* @since 2024年01月12日** 原子类测试*/
public class AtomicTest {private AtomicInteger i = new AtomicInteger(0);public static void main(String[] args) {AtomicTest test = new AtomicTest();// 线程池,50个固定线程ExecutorService executorService = Executors.newFixedThreadPool(50);CountDownLatch countDownLatch = new CountDownLatch(5000);for (int i = 0; i < 5000; i++) {executorService.execute(() -> {test.i.incrementAndGet();countDownLatch.countDown();});}executorService.shutdown();try {countDownLatch.await();System.out.println("执行完成后,i = " + test.i);} catch (InterruptedException e) {e.printStackTrace();}}
}

我们将变量的类型改为AtomicInteger ,AtomicInteger 是一个原子类。我们在之前调用i++的地方改成了i.incrementAndGet(),incrementAndGet()方法采用了CAS机制,也就是说使用了乐观锁。我们再运行一下程序,看看结果如何。

执行完成后,i=5000

执行完成后,i=5000

执行完成后,i=5000

我们同样执行了3次,3次的结果都是5000,符合了我们预期。这个就是乐观锁。我们对乐观锁稍加总结,乐观锁在读取数据的时候不做任何限制,而是在更新数据的时候,进行数据的比较,保证数据的版本一致时再更新数据。根据它的这个特点,可以看出乐观锁适用于读操作多,而写操作少的场景。

悲观锁与乐观锁恰恰相反,悲观锁从读取数据的时候就显示的加锁,直到数据更新完成,释放锁为止。在这期间只能有一个线程去操作,其他的线程只能等待。在JAVA中,悲观锁可以使用synchronized关键字或者ReentrantLock类来实现。还是,上面的例子,我们分别使用这两种方式来实现一下。首先是使用synchronized关键字来实现:

package cn.pottercoding.lock;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** @author 程序员波特* @since 2024年01月12日** 使用 synchronized 关键字来实现自增*/
public class SynchronizedTest {private int i = 0;public static void main(String[] args) {SynchronizedTest test = new SynchronizedTest();// 线程池,50个固定线程ExecutorService executorService = Executors.newFixedThreadPool(50);CountDownLatch countDownLatch = new CountDownLatch(5000);for (int i = 0; i < 5000; i++) {executorService.execute(() -> {// 修改部分,开始synchronized (test) {test.i++;}// 修改部分结束countDownLatch.countDown();});}executorService.shutdown();try {countDownLatch.await();System.out.println("执行完成后,i = " + test.i);} catch (InterruptedException e) {e.printStackTrace();}}
}

我们唯一的改动就是增加了synchronized块,它锁住的对象是test,在所有线程中,谁获得了test对象的锁,谁才能执行i++操作。我们使用了synchronized悲观锁的方式,使得i++线程安全我们运行一下,看看结果如何。

执行完成后,i=5000

执行完成后,i=5000

执行完成后,i=5000

我们运行3次,结果都是5000,符合预期。接下来,我们再使用Reent rantLock类来实现悲观锁。代码如下:

package cn.pottercoding.lock;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** @author 程序员波特* @since 2024年01月12日*/
public class LockTest {private int i = 0;Lock lock = new ReentrantLock();public static void main(String[] args) {LockTest test = new LockTest();// 线程池,50个固定线程ExecutorService executorService = Executors.newFixedThreadPool(50);CountDownLatch countDownLatch = new CountDownLatch(5000);for (int i = 0; i < 5000; i++) {executorService.execute(() -> {// 修改部分,开始test.lock.lock();test.i++;test.lock.unlock();// 修改部分结束countDownLatch.countDown();});}executorService.shutdown();try {countDownLatch.await();System.out.println("执行完成后,i = " + test.i);} catch (InterruptedException e) {e.printStackTrace();}}
}

我们在类中显示的增加了 Lock lock= new ReentrantLock();,而且在i++之前增加了 lock.lock(),加锁操作,在i++之后增加了lock.unlock()释放锁的操作。我们同样运行3次,看看结果。

执行完成后,i=5000

执行完成后,i=5000

执行完成后,i=5000

3次运行结果都是5000,完全符合预期。我们再来总结一下悲观锁,悲观锁从读取数据的时候就加了锁,而且在更新数据的时候,保证只有一个线程在执行更新操作,没有像乐观锁那样进行数据版本的比较。所以悲观锁适用于读相对少,写相对多的操作。

公平锁与非公平锁

前面我们介绍了乐观锁与悲观锁,这一小节我们将从另外一个维度去讲解锁一公平锁与非公平锁。从名字不难看出,公平锁在多线程情况下,对待每一个线程都是公平的;而非公平锁恰好与之相反。从字面上理解还是有些晦涩难懂,我们还是举例说明,场景还是去超市买东西,在储物柜存储东西的例子。储物柜只有一个,同时来了3个人使用储物柜,这时A先抢到了柜子,A去使用,B和C自觉进行排队。A使用完以后,后面排队中的第一个人将继续使用柜子,这就是公平锁。在公平锁当中,所有的线程都自觉排队,一个线程执行完以后,排在后面的线程继续使用。

非公平锁则不然,A在使用柜子的时候,B和C并不会排队,A使用完以后,将柜子的钥匙往后一抛,B和C谁抢到了谁用,甚至可能突然跑来一个D,这个D抢到了钥匙,那么D将使用柜子,这个就是非公平锁。

公平锁如图所示:

多个线程同时执行方法,线程A抢到了锁,A可以执行方法。其他线程则在队列里进行排队,A执行完方法后,会从队列里取出下一个线程B,再去执行方法。以此类推,对于每一个线程来说都是公平的,不会存在后加入的线程先执行的情况。

非公平锁入下图所示:

多个线程同时执行方法,线程A抢到了锁,A可以执行方法。其他的线程并没有排队,A执行完方法,释放锁后,其他的线程谁抢到了锁,谁去执行方法。会存在后加入的线程,反而先抢到锁的情况。

公平锁与非公平锁都在ReentrantLock类里给出了实现,我们看一下 ReentrantLock的源码。

ReentrantLock有两个构造方法,默认的构造方法中,sync=new NonfairSync();我们可以从字面意思看出它是一个非公平锁。再看看第二个构造方法,它需要传入一个参数,参数是一个布尔型true 是公平锁,false 是非公平锁。从上面的源码我们可以看出sync 有两个实现类,分别是FairSyncNonfairSync,我们再看看获取锁的核心方法,首先是公平锁FairSync 的,

然后是非公平锁NonfairSync的,

通过对比两个方法,我们可以看出唯一的不同之处在于!hasQueuedPredecessors()这个方法,很明显这个方法是一个队列,由此可以推断,公平锁是将所有的线程放在一个队列中,一个线程执行完成后,从队列中取出下一个线程,而非公平锁则没有这个队列。这些都是公平锁与非公平锁底层的实现原理,我们在使用的时候不用追到这么深层次的代码,只需要了解公平锁与非公平锁的含义,并且在调用构造方法时,传入 truefalse即可。

总结

JAVA中锁的种类非常多,在这一节中,我们找了非常典型的几个锁的类型给大家做了介绍。乐观锁与悲观锁是最基础的,也是大家必须掌握的。大家在工作中不可避免的都要使用到乐观锁和悲观锁。从公平锁与非公平锁这个维度上看,大家平时使用的都是非公平锁,这也是默认的锁的类型。如果要使用公平锁,大家可以在秒杀的场景下使用,在秒杀的场景下,是遵循先到先得的原则,是需要排队的,所以这种场景下是最适合使用公平锁的。

本文已收录至的我的公众号【程序员波特】,关注我,第一时间获取我的最新动态。

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

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

相关文章

Java 面试题 - 多线程并发篇

线程基础 创建线程有几种方式 继承Thread类 可以创建一个继承自Thread类的子类&#xff0c;并重写其run()方法来定义线程的行为。然后可以通过创建该子类的实例来启动线程。 示例代码&#xff1a; class MyThread extends Thread {public void run() {// 定义线程的行为} …

JUC02同步和锁

同步&锁 相关笔记&#xff1a;www.zgtsky.top 临界区 临界资源&#xff1a;一次仅允许一个进程使用的资源成为临界资源 临界区&#xff1a;访问临界资源的代码块 竞态条件&#xff1a;多个线程在临界区内执行&#xff0c;由于代码的执行序列不同而导致结果无法预测&am…

近视的孩子用什么灯?学生考研护眼台灯推荐

随着时代快速发展&#xff0c;2022年我国近视人数达到了7亿&#xff0c;呈现低龄化趋势&#xff0c;儿童及青少年人数占了53.8%。现在学业负担都很重&#xff0c;每个家长都不希望自己的孩子近视或加深近视了&#xff0c;都会想尽一切办法保护视力。而护眼台灯就成了家长购买台…

Qt中QGraphicsView架构下实时鼠标绘制图形

上一章节介绍了关于QGraphicsView的基础讲解&#xff0c;以及简单的类图创建&#xff0c;由上一章节中最后展示的动画效果来看&#xff0c;今年主要讲述如何在QGraphicsView架构下&#xff0c;实时拖动鼠标绘制图形&#xff01; 今天主要以矩形为例&#xff0c;再来看一下展示…

苹果电脑RAW图像处理软件Capture One Pro 22 mac软件介绍

Capture One Pro 22 for mac是一款专业的RAW文件转换器和图像编辑软件&#xff0c;拥有更新的处理引擎、市场领先的性能和强大的新功能&#xff0c;可为 500 多台高端相机提供具有美丽色彩和令人难以置信的细节的终极图像质量。 Capture One Pro 22 for Mac版软件介绍 Capture…

Vue-17、Vue人员列表过滤(案例)

1、watch实现 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>列表渲染过滤</title><script type"text/javascript" src"https://cdn.jsdelivr.net/npm/vue2/dist/vue.js&qu…

JavaScript Web Worker用法指南

&#x1f9d1;‍&#x1f393; 个人主页&#xff1a;《爱蹦跶的大A阿》 &#x1f525;当前正在更新专栏&#xff1a;《VUE》 、《JavaScript保姆级教程》、《krpano》 ​ ​ ✨ 前言 Web Worker可以将耗时任务放到后台执行,避免阻塞UI。本文将详细介绍Web Worker的用法,让你…

FineBI实战项目一(22):各省份订单个数及订单总额分析开发

点击新建组件&#xff0c;创建各省份订单个数及订单总额组件。 选择自定义图表&#xff0c;将province拖拽到横轴&#xff0c;将cnt和total拖拽到纵轴。 调节纵轴的为指标并列。 修改横轴和纵轴的标题。 修改柱状图样式&#xff1a; 将组件拖拽到仪表板。 结果如下&#xff1a;…

【专业英语】计算机专业英语(第三版)清华大学出版社

专业英语 部分专业名词 短语 在这里插入图片描述

FlinkCDC的分析和应用代码

前言&#xff1a;原本想讲如何基于Flink实现定制化计算引擎的开发&#xff0c;并以FlinkCDC为例介绍&#xff1b;发现这两个在表达上不知以谁为主&#xff0c;所以先分析FlinkCDC的应用场景和技术实现原理&#xff0c;下一篇再去分析Flink能在哪些方面&#xff0c;做定制化计算…

U盘用完到底能不能直接拔?一篇搞懂

有没有人懂这种情况&#xff01;&#xff01; 传输完文件之后&#xff0c;觉得大功告成 以十分帅气的姿势 and 迅雷不及掩耳之势 “咻”地一下把U盘直接给……拔掉了…… 然后瞬间想起没有安全退出&#xff0c;陷入深深的懊悔…… &#xff08;甚至还要再花时间&#xff0…

WebServer 跑通/运行/测试(详解版)

&#x1f442; 椿 - 沈以诚 - 单曲 - 网易云音乐 目录 &#x1f382;前言 &#x1f33c;跑通 &#xff08;1&#xff09;系统环境 &#xff08;2&#xff09;克隆源码 &#xff08;3&#xff09;安装和配置 Mysql &#xff08;4&#xff09;写 sql 语句 &#xff08;5&…

win11下载Hbuliderx 安装闪退解决教程+安装包分享

在官网下载 目录 在官网下载 出现闪退 下载失败 2.2. 最终在百度网盘里下载了历史版本 2.3. 然后解压文件 2.4. 双击打开 2.5. 安装成功 出现闪退 下载失败 结果下载失败&#xff0c;一下子弹出的下载框就会闪退 2.2. 最终在百度网盘里下载了历史版本 下载的网盘链接: …

黑马苍穹外卖学习Day5

文章目录 Redis学习Redis简介准备工作Redis常用数据类型介绍各数据类型的特点Redis常用命令字符串操作命令哈希操作命令列表操作命令集合操作命令有序集合操作命令通用操作命令 在Java中操作Redis导入Spring Data Redis坐标配置Redis数据源编写配置类&#xff0c;创建RedisTemp…

linux多进程基础(2):僵尸进程以及解决方法wait()函数(大白话解释)

在我的linux多线程多进程基础专栏中,已和大家一起分享了僵尸线程.在这一篇文章中我将分享僵尸进程以及解决方法wait()函数. 1.僵尸进程 什么是僵尸进程呢?用最通俗易懂的话来说就是子进程执行结束的时候其父进程并没有及时回收该子进程导致成为僵尸进程.如果僵尸进程数量较多…

10分钟快速搭建个人博客、文档网站!

本文来分享 8 个现代化前端工具&#xff0c;帮你快速生成个人博客、文档网站&#xff01; VitePress VitePress 是一款静态站点生成器&#xff0c;专为构建快速、以内容为中心的网站而设计。简而言之&#xff0c;VitePress 获取用 Markdown 编写的源内容&#xff0c;为其应用…

python24.1.13for循环

对列表、字典、字符串等进行迭代 range

Legion R7000 2021(82JW)原装出厂Win10/WIN11系统预装OEM系统镜像

LENOVO联想拯救者R7000 2021款(82JW)笔记本电脑原厂Windows10/11系统 链接&#xff1a;https://pan.baidu.com/s/1m_Ql5qu6tnw62PbpvXB0hQ?pwd6ek4 提取码&#xff1a;6ek4 原装出厂系统自带所有驱动、出厂主题壁纸、系统属性专属联机支持标志、系统属性专属联想的LOGO标…

88.乐理基础-记号篇-反复记号(二)D.C.、D.S.、Fine、Coda

内容参考于&#xff1a;三分钟音乐社 上一个内容&#xff1a;87.乐理基础-记号篇-反复记号&#xff08;一&#xff09;反复、跳房子-CSDN博客 下图红色左括号框起来的东西&#xff0c;它们都相对比较抽象一点&#xff0c;这几个词都是意大利语 首先D.C.这个标记&#xff0c;然…

7 - MySQL主从同步|主从同步模式

MySQL主从同步&#xff5c;主从同步模式 MySQL主从同步主从同步介绍主从同步工作过程主从同步结构模式配置主从同步一主一从同步结构一主多从同步结构主从从同步结构主主同步结构 主从同步模式主从同步结构模式复制模式 MySQL主从同步 主从同步介绍 存储数据的服务结构 主服务…