深入解析Java中锁机制以及底层原理

news2024/11/18 14:24:02

一、概述

1.1 背景

概念:锁是多线程编程中的机制,用于控制对共享资源的访问。可以防止多个线程同时修改或读取共享资源,从而保证线程安全。
作用:锁用于实现线程间的互斥和协调,确保在多线程环境下对共享资源的访问顺序正确性

1.2 锁分类

【按锁性质划分】:

  • 乐观锁:认为一个线程获取共享数据的时候,不会存在其他线程修改该共享数据的情况,所以不会上锁。例如:CAS机制版本号机制
  • 悲观锁:认为一个线程获取共享数据时,一定会存在其他线程修改该共享数据的情况,因此,获取共享数据时都会进行加锁。例如:Synchronized锁ReentrantLock锁

【按锁被持有数量划分】:

  • 独占锁:当前锁只有被一个线程持有。例如:ReentrantLock锁
  • 共享锁:当前锁可以被多个线程持有。例如:Semaphore等。

【按公平性划分】:

  • 公平锁:多个线程竞争锁时,需要进行排队,按照先来后到顺序获取锁。例如:ReentrantLock公平锁
  • 非公平锁:多个线程竞争锁时,先进行插队,插入失败再排队。例如:Synchronized锁ReentrantLock锁

【按可重入性划分】:

  • 可重入锁:允许一个线程多次加锁。例如:Synchronized锁ReentrantLock锁
  • 不可重入锁:允许一个线程仅加锁一次

【按锁范围划分】:

  • 单体锁:仅能锁住当前JVM进程中的共享资源,对其他JVM进程中的共享资源不起作用。例如: Synchronized锁ReentrantLock锁
  • 分布式锁:借助中间件,对多个JVM进程中的同一共享资源都能锁住。例如:Redis分布式锁

二、单JVM进程锁

2.1 独占锁

2.1.1 synchronized锁

详情见:深入解析Synchronized锁底层原理
局限性:

  1. 是否释放锁,开发者无法自己控制,导致其他线程只能一直阻塞;
  2. 若获取锁的线程进入休眠或阻塞,除了线程出现异常,否则其他线程将会一直阻塞等待。
    因此,在JDK1.5后,加入了Doug Lea大神贡献的java.util.concurrent包,包内提供了Lock类,提供了更加灵活控制锁的功能,弥补了Synchronized的缺陷。

2.1.2 ReentrantLock锁

Lock完全是由Java编写,提供了锁获取和释放的控制权、可中断的获取锁以及超时获取锁等多种高级特性。Lock只是一个接口,常见的实现类有:

1. 重入锁:ReentrantLock;
2. 读锁:ReadLock
3. 写锁:WriteLock

但底层都是通过聚合了一个java 同步器(AbstractQueueSynchronizer, AQS)来完成线程的访问控制的。因此,需要提前了解AQS的底层原理。详情见:深入解析AQS队列同步器的底层原理

ReentrantLock实现了Lock接口,同时底层通过聚合AQS完成并发的功能【注意:此时state只能为0或1】。主要有以下特点:

1. 支持重进入的锁,表示该锁能够支持一个线程对资源的重复加载,同时还支持获取锁的公平和非公平性。
2. 构造方法会接收一个可选的公平参数(默认是非公平锁)。设置为true时,表示公平锁;否则为非公平锁。
3. 可重入性的体现:任意线程获取锁之后,再次获取该锁时,不会被锁所阻塞。因为是可重入的,有一个计数器记录重入次数n, 当n = 0时表明锁完全被释放。

ReentrantLock实现的公平锁和非公平锁的区别:

1. 获取锁的时候是否按照FIFO的顺序来的。公平锁不仅会对state状态进行判断,还会判断当前同步队列中是否有元素,如果存在元素,则插入到同步队列的尾部,真正的先来后到;
2. 非公平锁性能高于公平锁性能。非公平锁可以减少CPU唤醒线程的开销,整体的吞吐率会高点,CPU也不会唤醒所有的线程,减少唤醒线程的数量。具体原因为:
【公平锁获取锁:】会将线程自己添加到等待队列的队尾并休眠,当某线程用完锁之后,会去唤醒等待队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序。在整个过程中,线程会从运行状态切换到休眠状态,再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态,而这个状态的转换是比较慢的,所以公平锁的执行速度会比较慢。
】非公平锁获取锁:】当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。这样做的好处是,获取锁不用遵循先到先得的规则,从而避免了线程休眠和恢复的操作,这样就加速了程序的执行效率。
3. 非公平锁会存在线程饥饿的情况。但出现线程饥饿的机率非常低可以忽略不记。这就是默认非公平锁的原因。

2.1.3 局限性

synchronized和ReentrantLock锁,一次仅允许一个线程访问资源,即属于独占性。对于多个线程同时访问共享资源的场景,是无能为力的。不过Java也提供了对应的解决方案:java Semaphore信号量、CountDownLatch以及CyclicBarrier共享锁

2.2 共享锁

java Semaphore信号量、CountDownLatch以及CyclicBarrier共享锁底层的原理是相通的,都是基于AQS队列同步器来实现的。相比于独占锁,主要区别在于state值设置可由开发者进行控制,这样就可以实现多个线程同时访问共享资源。AQS底层原理见:深入解析AQS队列同步器的底层原理。

2.2.1 Semaphore

Semaphore(信号量)为多线程协作提供了更为强大的控制方法,默认是非公平的。
常用场景限流,尤其是公共资源有限的应用场景,例如数据库连接,停车场车位数等。

2.2.2 CountDownLatch

CountDownLatch称之为闭锁,它可以使一个或一批线程在闭锁上等待,等到其他线程执行完相应操作后,闭锁打开,这些等待的线程才可以继续执行。确切的说,闭锁在内部维护了一个倒计数器。通过该计数器的值来决定闭锁的状态,从而决定是否允许等待的线程继续执行,是批量Join的实现方案。

2.2.3 CyclicBarrier

CyclicBarrier通常称为循环屏障。它和CountDownLatch很相似,都可以使线程先等待然后再执行。不过CountDownLatch是使一批线程等待另一批线程执行完后再执行;而CyclicBarrier只是使等待的线程达到一定数目后再让它们继续执行。故而CyclicBarrier内部也有一个计数器,计数器的初始值在创建对象时通过构造参数指定。
场景:可循环使用的屏障。即等待一组线程到达一个屏障时被阻塞,直到最后一个线程到达,才会执行。例:五个人一组玩游戏,先到的进行等待,直到凑齐五个人才开始执行任务。

2.2.4 CyclicBarrier与CountDownLatch的区别:

  1. CyclicBarrier的计数器可以重置而CountDownLatch不行,这意味着CyclicBarrier实例可以被重复使用而CountDownLatch只能被使用一次;
  2. CyclicBarrier还有getNumberWaiting()方法获取阻塞线程数量;isBroken()方法用来了解阻塞的线程是否被中断。
  3. CountDownLatch指的是每个线程的主业务逻辑执行完成后,再统一释放锁;而CyclicBarrier指的是等指定数量线程准备好后,再执行主业务逻辑。

2.3 总结

不管是独占锁还是共享锁,解决的是共享资源的访问控制问题,无法解决线程见的通信问题。对应的解决方案有:

1. Synchronized锁配合Object的wait和notify等方法来实现线程通信;
2. ReentrantLock锁配合Condition实现多个条件下的线程通信。

Condition的底层实现原理见:深入解析Condition的底层实现原理。
上述相关的锁的实现,底层都离不开CAS机制和Volatile。因此,有必要了解CAS底层的实现原理,详情见:深入解析CAS的原理机制。

三、分布式锁

解决的问题:保证一个方法在同一时间内只能被同一个线程执行,在单体应用下,单体锁只能锁住一个JVM进程,其他进程不受影响,显然是无法满足我们的要求的。要考虑非阻塞式分布式锁和阻塞式分布式锁,要根据业务来进行考虑。
分布式锁的要求:

  1. 保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行;
  2. 是一把可重入锁(防止死锁);
  3. 阻塞锁(根据业务考虑阻塞或非阻塞);
  4. 高可用的获取锁和释放锁功能;
  5. 获取锁和释放锁的性能要好

3.1 数据库分布式锁

多个进程多个线程访问共同组件数据库,专门建立一个数据库一张表存放用户自定义锁。

3.1.1 基于数据库表

当想要锁住一个方法或资源时,直接将方法或资源信息插入到表中, 同时在数据库层面对方法或资源信息添加唯一性约束,这样当插入成功时,就表示获取到锁;释放锁的时候直接删除信息即可。例如:
锁信息表:

CREATE TABLE `methodLock` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名',
  `desc` varchar(1024) NOT NULL DEFAULT '备注信息',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
) ENG

加锁:

insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)

解锁:

delete from methodLock where method_name ='method_name'

存在的问题:

  1. 可用性:强依赖数据库的可用性,一旦数据库宕机,会导致业务系统不可用;
  2. 自动释放:由于无法设置失效时间,一旦解锁失败,那么其他线程将无法获取到锁;
  3. 阻塞性:插入数据失败的线程会直接报错,返回报错信息,不会等待,因此对某些业务来说是不可接受的;
  4. 可重入性:该锁是非重入锁,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了

问题解决方法:

  • 问题1:数据库集群部署,但为了使用分布式锁,多部署一个集群,性价比低,同时高并发情况下,数据库会宕机;
  • 问题2:后台启动一个定时任务, 定期清理数据表无用的数据。某一时间点占用了大量的数据库连接;
  • 问题3:设置一个while循环,直到insert成功。性能非常差,产生大量无效insert行为;
  • 问题4:在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。

3.1.2 基于数据库排他锁

具体SQL语句:

select ... for update
注意: 
1:要配合事务使用,才会有效使用事务将要加锁的地方包裹住,等执行完后,再进行提交。
因为如果select .. for update后就提交事务
2Innodb只针对根据索引查询来添加行锁,否则添加表级锁

加锁:

select ... for update

解锁:

应用层面:自己实现事务的提交
public void unlock(){
    connection.commit();
}

该中方式解决了基于数据库表的阻塞和无法释放锁的问题:

  1. 阻塞性:当select … for update时,会被数据库阻塞住,直到查询数据才会返回;
  2. 自动释放锁:若数据库宕机,会自动释放锁

存在的问题:

  1. 单点故障以及可重入问题;
  2. 是否走索引不确定,导致使用的是表锁:MySql会对查询进行优化,即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的,如果 MySQL 认为全表扫效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁;
  3. 数据库性能:一个排他锁长时间不提交,就会占用数据库连接。一旦类似的连接变得多了,就可能把数据库连接池撑爆

注意: 将作为锁的数据库与业务数据库分开。

3.2 基于缓存实现分布式锁

3.2.1 Redis分布式锁

使用Redis的setnx实现分布式锁。命令如下:

set resource_name my_random_value NX PX 30000
resource_name: 资源名称,可根据不同业务区分不同的锁;
my_random_value: 随机值,每个线程的随机值都不同,用户释放锁时的校验。一般采用UUID
NX:key不存在时设置成功,key存在时则设置不成功
PX:自动失效时间,出现异常情况,锁可以过期失效

实现原理:

  1. 利用NX的原子性,多个线程并发时,只有一个线程可以设置成功;
  2. 设置成功即可获得锁,可以执行后续的业务处理;
  3. 如果出现异常,过了锁的有效期,锁自动释放;
  4. 释放锁采用delete命令;
  5. 释放锁时校验之前设置的随机值,相同才释放;
  6. 释放锁的LUA脚本【先校验后释放】。原因:有A和B两个线程, 若A先获取锁,由于某些原因,A超时了,导致A的锁被释放,此时B获取到了锁,然后执行A释放锁的操作,此时会释放掉B持有的锁。【产生并发问题,所以释放和校验要使用LUA脚本来实现】

优点:

  1. 可自动释放锁:设置过期时间;
  2. 可靠性高:集群部署

缺点:

  1. 无法缓存层面实现阻塞:只能应用层面实现
  2. 无法实现可重入性

3.2.2 基于Redisson实现分布式锁

在Redis基础上,利用Java对Redis客户端进行封装,并对单体应用下的JDK并发包和JDK集合类等进行扩展,提供分布式下的解决方案。

RLock lock = redisson.getLock();

3.3 基于Zookeeper分布式锁

3.3.1 基于Zookeeper的瞬时节点实现分布式锁

3.3.1.1 前言

取决于Zookeeper内部的命名空间模型结构。该命名空间模型类似于Linux文件结构,采用树状结构,各个节点被称为znode。每个节点可以存储路径以及与之相关的元数据,还有子节点列表。
在这里插入图片描述

3.3.1.2 节点类型

在这里插入图片描述

3.3.1.3 基于临时顺序节点的分布式锁

核心思想:临时顺序节点 + Watch(观察器)机制
实现原理:

  1. 多线程并发创建多个瞬时节点,得到有序的瞬时节点列表;
  2. 选用序号最小的线程获取锁;
  3. 其他线程则利用Watch机制监听自己序号的前一个序号;
  4. 前一个线程执行完成,删除自己序号的节点,利用线程的wait和notify来阻塞和唤醒对象的线程获取锁。

优点:

  1. 锁自动释放:一旦zookeeper宕机或session断开,瞬时节点就会被删除,因此锁就被释放了;
  2. 可阻塞:用Zookeeper可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。底层使用的是wait和notify机制,因此是阻塞的
    3.可重入:客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队;
  3. 单点被解决:zookeeper通常是集群部署的。

3.3.2 基于Zookeeper的Curator客户端实现分布式锁

使用Java对Zookeeper客户端进行进一步封装,并提供许多简单便利的功能,比如分布式锁java InterProcessMutex
缺点: 频繁的创建和删除瞬时节点,ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同步到所有的Follower机器上,性能有影响。

3.4 分布式锁对比

易于理解程度(从低到高):数据库 > 缓存 > zookeeper;
实现复杂度(从低到高):zookeeper > 缓存 > 数据库
性能(从低到高): 缓存 > zookeeper > 数据库
可靠性(从低到高):数据库 < 缓存 < zookeeper
不推荐使用自己编写的分布式锁,推荐使用RedissonCurator实现的分布式锁

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

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

相关文章

如何处理Android悬浮弹窗双击返回事件?

目录 1 前言 1.1 准备知识 1.2 问题概述 2 解决方案 3 代码部分 3.1 动态更新窗口焦点 3.2 窗口监听返回事件 3.3 判断焦点是否在窗口内部 3.4 窗口监听焦点移入/移出 1 前言 1.1 准备知识 1&#xff09;开发环境&#xff1a; 2D开发环境&#xff1a;所有界面或弹窗…

FFmpeg工作流程及视频文件分析

FFmpeg工作流程: 解封装(Demuxing)--->解码(Decoding)--->编码(Encoding)--->封装(Muxing) FFmpeg转码工作流程: 读取输入流--->音视频解封装--->解码音视频帧--->编码音视频帧--->音视频封装--->输出目标流 可简单理解为如下流程: 读文件-->解…

教师如何搭建学生查询考试分数的平台?

随着信息技术的快速发展&#xff0c;搭建一个学生查询考试分数的平台已经成为现代教育管理的重要组成部分。这样的平台不仅可以提高成绩管理的效率&#xff0c;还能为学生提供便捷、及时的成绩查询服务。那么&#xff0c;作为教师&#xff0c;我们应该如何搭建这样一个平台呢&a…

计算机网络期末98+冲刺笔记

一、计算机网络基础 1.1计算机网络的概述 计算机网络的定义&#xff1a;利用通信设备和线路&#xff0c;将地理位置不同的具有独立功能的多台计算机机器外部设备连接起来&#xff0c;在网络操作系统、网络管理软件及网络通信协议的管理和协调下&#xff0c;实现资源共享和信息…

力扣977. 有序数组的平方

思路&#xff1a;暴力法&#xff1a;全部平方&#xff0c;然后调用排序API&#xff0c;排序算法最快是N*log(N)时间复制度。 双指针法&#xff1a;要利用好原本的数组本就是有序的数组这个条件&#xff0c; 只是有负数 导致平方后变大了&#xff0c;那么平方后的最大值就是在两…

YOLOv5+DeepSort的汽车流量统计

前言 先来看下实现效果&#xff1a; 上图展示了用yolov5作为检测器&#xff0c;DeepSort为追踪器实现了对车流量的统计并绘制了每辆车的运行轨迹。 一、整体目录结构 下图展示了项目的整体目录结构&#xff1a; 其中&#xff1a; deep_sort文件下为目标跟踪相关代码&#x…

力扣串题:验证回文串2

bool judge(char *s,int m,int n){while(m<n){if(s[m]!s[n]){return false;}m,n--;}return true; } bool validPalindrome(char * s){for(int i0,jstrlen(s)-1;i<j;i,j--){if(s[i]!s[j]){return (judge(s,i1,j)||judge(s,i,j-1));}}return true; }这个题直接背大佬代码吧…

记录一下在Pycharm中虚拟环境的创建

如果在Pycharm中要新建一个虚拟环境&#xff0c;那你可以在Terminal中选择Command Prompt&#xff0c;在这里面执行相关命令 一、安装了Anaconda&#xff0c;创建虚拟环境 当你使用解释器是Anaconda提供的时&#xff0c;你可以使用conda命令执行&#xff0c;见以下操作&#x…

自适应窗口图片轮播HTML代码

自适应窗口图片轮播HTML代码&#xff0c;源码由HTMLCSSJS组成&#xff0c;记事本打开源码文件可以进行内容文字之类的修改&#xff0c;双击html文件可以本地运行效果&#xff0c;也可以上传到服务器里面&#xff0c;重定向这个界面 代码下载地址 自适应窗口图片轮播HTML代码

pkav之当php懈垢windows通用上传缺陷

环境&#xff1a; Windowsnginxphp 一、php源码 <?php //U-Mail demo ... if(isset($_POST[submit])){$filename $_POST[filename];$filename preg_replace("/[^\w]/i", "", $filename);$upfile $_FILES[file][name];$upfile str_replace(;,&qu…

【Flink SQL】Flink SQL 基础概念:SQL 动态表 连续查询

Flink SQL 基础概念&#xff1a;SQL 动态表 & 连续查询 1.SQL 应用于流处理的思路2.流批处理的异同点及将 SQL 应用于流处理核心解决的问题3.SQL 流处理的输入&#xff1a;输入流映射为 SQL 动态输入表4.SQL 流处理的计算&#xff1a;实时处理底层技术 - SQL 连续查询5.SQL…

Windows Server 各版本搭建 Web 服务器实现访问本地 Web 网站(03~19)

一、Windows Server 2003 点击左下角开始➡管理工具➡管理您的服务器&#xff0c;点击添加或删除角色 点击下一步 选择自定义&#xff0c;点击下一步 选择应用程序服务器&#xff0c;点击下一步 不勾选&#xff0c;点击下一步 这里提示插入磁盘&#xff0c;咱们提前下载好 IIS…

SinoDB海洋渔业时序数据解决方案

一、海洋渔业平台 介绍 福建理工大学针对我国浅海增养殖信息化和智能化程度低、多源数据库缺乏、大数据挖掘与分析技术薄弱等问题&#xff0c;构建了海洋渔业平台。 该平台方案使用了星瑞格数据库管理系统&#xff08;下文简称&#xff1a;SinoDB&#xff09;&#xff0c;充分利…

在Linux中进行OpenSSH升级

由于OpenSSH有严重漏洞&#xff0c;因此需要升级OpenSSH到最新版本。 OpenSSL和OpenSSH都要更新&#xff0c;OpenSSH依赖于OpenSSL。 第一步&#xff0c;查看当前的OpenSSH服务版本。 命令&#xff1a;ssh -V 第二步&#xff0c;安装、启动telnet&#xff0c;关闭安全文件&a…

使用CrossOver 在Mac 运行Windows 软件|D3DMetal是什么技术,

CrossOver Mac 使用特点 • 免费试用 14 天&#xff0c;可使用 CrossOver Mac 全部功能&#xff0c;• 试用过期会保留之前安装的 Windows 软件• 使 Mac 运行 Windows 程序 使用CrossOver在Mac上运行Windows软件是一个方便且无需安装完整Windows操作系统的解决方案。CrossOve…

Linux下的编辑器——Vim

vi/vim 的区别简单点来说&#xff0c;它们都是多模式编辑器&#xff0c;不同的是 vim 是 vi 的升级版本&#xff0c;它不仅兼容 vi 的所有指令&#xff0c;而且还有一些新的特性在里面。例如语法加亮&#xff0c;可视化操作不仅可以在终端运行&#xff0c;也可以运行于x window…

SpringBoot(Lombok + Spring Initailizr + yaml)

1.Lombok 1.基本介绍 2.应用实例 1.pom.xml 引入Lombok&#xff0c;使用版本仲裁 <!--导入springboot父工程--><parent><artifactId>spring-boot-starter-parent</artifactId><groupId>org.springframework.boot</groupId><version&g…

20240313寻找集成联调交付的具体方式

集成联调交付&#xff08;Integrated Joint Debugging and Delivery&#xff09;是软件开发过程中的一个阶段&#xff0c;主要涉及将不同的软件模块或组件整合在一起&#xff0c;并进行联合调试和测试&#xff0c;以确保它们能够作为一个整体正常工作。这个过程通常发生在开发周…

DMSP夜间灯光卫星介绍和数据下载

DMSP(Defense Meteorological Sate-llite Program)是美国国防部的极轨卫星计划&#xff0c;与NOAA卫星同属于一类&#xff0c;只不过星上载荷不同。 DMSP卫星简介 现有DMSP为三轴姿态稳定卫星&#xff0c;运行在高度约830km的太阳同步轨道&#xff0c;周期约101min&#xff0c…

安卓六大布局

LinearLayout&#xff08;线性布局&#xff09; 1.简介 线性布局在开发中使用最多&#xff0c;具有垂直方向与水平方向的布局方式。LinearLayout 默认是垂直排列的&#xff0c;但是可以通过设置 android:orientation 属性来改变为水平排列。 2.常用属性 orientation&#xf…