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

一、概述

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.mzph.cn/news/743854.shtml

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

相关文章

Flutter开发入门——Widget和常用组件

1.什么是Widget&#xff1f; 在Flutter中几乎所有的对象都是一个 widget 。与原生开发中“控件”不同的是&#xff0c;Flutter 中的 widget 的概念更广泛&#xff0c;它不仅可以表示UI元素&#xff0c;也可以表示一些功能性的组件如&#xff1a;用于手势检测的 GestureDetecto…

spring中事务失效的场景有哪些?

异常捕获处理 在方法中已经将异常捕获处理掉并没有抛出。 事务只有捕捉到了抛出的异常才可以进行处理&#xff0c;如果有异常业务中直接捕获处理掉没有抛出&#xff0c;事务是无法感知到的。 解决&#xff1a;在catch块throw抛出异常。 抛出检查异常 spring默认只会回滚非检…

ChatGPT浪潮来袭!谁先掌握,谁将领先!

任正非在接受采访时说 今后职场上只有两种人&#xff0c; 一种是熟练使用AI的人&#xff0c; 另一种是创造AI工具的人。 虽然这个现实听起来有些夸张的残酷&#xff0c; 但这就是我们必须面对的事实 &#x1f4c6; 对于我们普通人来说&#xff0c;我们需要努力成为能够掌握…

基于STM32的智慧农业管理系统设计与实现

文章目录 一、前言1.1 项目介绍【1】项目功能【2】设计实现的功能【3】项目硬件模块组成 1.2 设计思路1.3 传感器功能介绍1.4 开发工具的选择 二、EMQX开源MQTT服务器框架三、购买ECS云服务器3.1 登录官网3.2 购买ECS服务器3.3 配置安全组3.4 安装FinalShell3.5 远程登录到云服…

xsslabs靶场通关(持续更新)

文章目录 前言一、level1思路实现 二、levle2思路 三、level3思路实现 四、level4思路实现 五、level5思路实现 六、level6思路实现 七、level7思路实现 八、level8思路实现 九、level9思路实现 前言 本篇文章将介绍在xsslabs这个靶场&#xff08;在不知道源码的前提下&#x…

Linux从0到1——Linux环境基础开发工具的使用(上)

Linux从0到1——Linux环境基础开发工具的使用&#xff08;上&#xff09; 1. Linux软件包管理器yum1.1 yum介绍1.2 用yum来下载软件1.3 更新yum源 2. Linux编辑器&#xff1a;vi/vim2.1 vim的基本概念2.2 vim的基本操作2.3 vim正常模式命令集2.4 vim底行模式命令集2.5 视图模式…

Java初阶数据结构队列的实现

1.队列的概念 1.队列就是相当于排队打饭 2.在排队的时候就有一个队头一个队尾。 3.从队尾进对头出 4.所以他的特点就是先进先出 所以我们可以用链表来实现 单链表实现要队尾进队头出{要有last 尾插头删} 双向链表实现效率高&#xff1a;不管从哪个地方当作队列都是可以的&…

OpenMP 编程模型

OpenMP 内存模型 共享内存模型&#xff1a; OpenMP 专为多处理器/核心、共享内存机器设计&#xff0c;底层架构可以是共享内存UMA或NUM OpenMP 执行模型 基于线程的并行&#xff1a; OpenMP 程序基于多线程来实现并行&#xff0c; 线程是操作系统可以调度的最小执行单元。 …

react 综合题-旧版

一、组件基础 1. React 事件机制 javascript 复制代码<div onClick{this.handleClick.bind(this)}>点我</div> React并不是将click事件绑定到了div的真实DOM上&#xff0c;而是在document处监听了所有的事件&#xff0c;当事件发生并且冒泡到document处的时候&a…

Facebook:连接世界的社交巨人

在当今数字化时代&#xff0c;Facebook作为全球最大的社交媒体平台之一&#xff0c;扮演着连接世界的重要角色。它不仅仅是一个社交网站&#xff0c;更是一个数字化的社交生态系统&#xff0c;影响着全球数十亿用户的生活和交流方式。本文将深入探讨Facebook的起源、用户规模和…

uniapp——第1篇:基于vue语法的、比原生开发屌的小程序开发

前提&#xff0c;建议先学会前端几大基础&#xff1a;HTML、CSS、JS、Ajax&#xff0c;还有一定要会Vue!&#xff08;Vue2\Vue3&#xff09;都要会&#xff01;&#xff01;&#xff01;不然不好懂 博主作为大二前端小白&#xff0c;刚刚接触前端微信小程序开发时选择的是基于“…

electron + vtkjs加载模型异常,界面显示类似于图片加载失败的图标

electron vtkjs加载模型显示异常&#xff0c;类似于图片加载失败的效果&#xff0c;如上图。 electron开发版本&#xff1a;13。 解决方法&#xff1a;升级electron版本号。 注意&#xff1a;win7最高兼容electron 22版本。

多维时序 | Matlab实现VMD-CNN-GRU变分模态分解结合卷积神经网络门控循环单元多变量时间序列预测

多维时序 | Matlab实现VMD-CNN-GRU变分模态分解结合卷积神经网络门控循环单元多变量时间序列预测 目录 多维时序 | Matlab实现VMD-CNN-GRU变分模态分解结合卷积神经网络门控循环单元多变量时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现VMD-CN…

c++指针的定义和使用

1、定义一个指针 int a10; //定义指针的语法&#xff1a;数据类型 * 指针变量名&#xff1a;int * p&#xff1b; //让指针记录变量a的地址&#xff1a;p &a; int a 10;int* p; p &a; cout << "a的地址为&#xff1a;" << &a <<…

第二十二节 Java 流(Stream)、文件(File)和IO

Java.io 包几乎包含了所有操作输入、输出需要的类。所有这些流类代表了输入源和输出目标。 Java.io 包中的流支持很多种格式&#xff0c;比如&#xff1a;基本类型、对象、本地化字符集等等。 一个流可以理解为一个数据的序列。输入流表示从一个源读取数据&#xff0c;输出流…

社区居民医疗健康系统 微信小程序

设计原则 本社区健康医疗APP采用 Hbuildex技术&#xff0c;使用Java语言开发&#xff0c;充分保证了系统稳定性、完整性。 社区健康医疗APP的设计与实现的设计思想如下&#xff1a; &#xff08;1&#xff09;操作简单方便、系统界面安全良好、简单明了的页面布局、方便查询相…

GPT-4.5 Turbo意外曝光,最快明天发布?OpenAI终于要放大招了!

大家好&#xff0c;我是木易&#xff0c;一个持续关注AI领域的互联网技术产品经理&#xff0c;国内Top2本科&#xff0c;美国Top10 CS研究生&#xff0c;MBA。我坚信AI是普通人变强的“外挂”&#xff0c;所以创建了“AI信息Gap”这个公众号&#xff0c;专注于分享AI全维度知识…

【计算机网络篇】计算机网络的定义和分类

文章目录 &#x1f354;什么是计算机网络&#x1f5c3;️计算机网络的分类⭐按交换方式分类⭐按使用者分类⭐按传输介质分类⭐按覆盖范围分类⭐按拓扑结构分类 &#x1f6f8;小结 &#x1f354;什么是计算机网络 计算机网络是指将多台计算机或其他网络设备通过通信链路连接起来…

汇编语言程序设计 第2章:8086指令系统简介及寻址方式

文章目录 1. 指针的分类及格式1.1 指令的分类1.2 指令格式 2. 寻址方式MOV指令简介2.1 立即寻址2.2 寄存器寻址2.3 直接寻址2.4 寄存器间接寻址2.5 寄存器相对寻址2.6 基址变址寻址2.7 相对基址变址寻址 3. 数据传送指令3.1 通用数据传送指令3.2 地址传送指令&#xff08;LEA、…

C语言黑魔法第三弹——动态内存管理

本文由于排版问题&#xff0c;可能稍显枯燥&#xff0c;但里面知识点非常详细&#xff0c;建议耐心阅读&#xff0c;帮助你更好的理解动态内存管理这一C语言大杀器 进阶C语言中有三个知识点尤为重要&#xff1a;指针、结构体、动态内存管理&#xff0c;这三个知识点决定了我们…