Java精通 —— 一篇文章弄懂锁

news2024/7/6 19:37:45

前言

        在Java中为了保证操作线程的安全性,我们引入了锁的概念,但随之而来的性能问题让我们在不愿意放弃安全性保证的前提下提出了优化过的锁。在这篇文章中,荔枝会着重梳理不同的锁的概念和普通锁的执行机制相关知识,同时也会对Java中三个可以操作锁的类的用法和特点进行区分。希望能帮助到有需要的小伙伴~~~


文章目录

前言

一、线程安全

二、锁的基本概念

2.1 加锁本质理解 

2.2 锁的优化机制 

2.2.1 锁的粒度优化

2.2.2 无锁化编程和乐观锁

2.2.3 偏向锁、重量级锁和轻量级锁(减少锁竞争)

2.2.4 锁消除和锁膨胀

2.2.5 读写锁优化 

2.2.6 公平锁和非公平锁

2.3 锁的特性  

2.3.1 可重入锁

2.3.2 分布式锁 

三、Synchronized、Lock和ReentrantLock特点和区别 

3.1 Synchronized(关键字)

3.2 Lock(接口):

3.3 ReentrantLock(类):

3.4 区别

总结


一、线程安全

        Java中的锁和MySQL中的锁是类似的,只不过MySQL中的锁是针对于数据库事务,而Java中的锁是用来操作线程事务滴,但最终二者都是要确保数据一致性。也就是说之所以需要给线程加锁,是为了确保线程安全。在正式开始梳理锁的知识之前,我们需要明确线程安全的实现必须保证的三个维度:原子性、有序性和可见性。

什么是线程安全?

如果程序采用多线程的方式来同时运行某一段封装好的代码,如果运行结果与单线程运行的结果是一致的,那么这段程序就称之为线程安全的。

一个简单的例子来理解保证线程安全的必要性 

我们定义一个int类型的变量为1,这时候同时开启了两个线程Thread1和Thread2,这两个线程同时执行i++的操作,问这时候i的值是多少?

        由于开启了两个线程,我们先假设Thread1执行i++操作成功。看Thread2获取i的时机是在Thread1前还是后,如果是在Thread1返回了i值后,那么i的取值最后就是2;如果是在Thread1返回i值前,那么就会取到1。这时候我们无法确定i的值。

保证线程安全的三个维度 

  • 原子性:就是该线程的相关操作不会被其它的线程干扰,运行结果与通过同步机制来实现是一致的;
  • 可见性: 若一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile 就是负责保证可见性的;
  • 有序性:保证线程内串行语义,避免指令重排等,即保证程序执行的顺序按照代码的先后顺序执行。

实现线程安全

  • 原子性(Atomicity):单个或多个操作是要么全部执行,要么都不执行
  1. Lock:保证同时只有一个线程能拿到锁,并执行申请锁和释放锁的代码
  2. synchronized:对线程加独占锁,被它修饰的类/方法/变量只允许一个线程访问
  • 可见性(Visibility):当一个线程修改了共享变量的值,其他线程能够立即得知这个修改
  1. volatile:保证新值能立即同步到主内存,且每次使用前立即从主内存刷新;
  2. synchronized:在释放锁之前会将工作内存新值更新到主存中
  • 有序性(Ordering):程序代码按照指令顺序执行
  1. volatile: 本身就包含了禁止指令重排序的语义
  2. synchronized:保证一个变量在同一个时刻只允许一条线程对其进行lock操作,使得持有同一个锁的两个同步块只能串行地进入、

二、锁的基本概念

了解完线程安全,我们明白了给线程加锁的缘由。那么线程中的锁有哪些呢?按照锁的功能分类,可以将所有的锁分为两类:共享锁(读锁)和排他锁(写锁)。

  • 共享锁: 允许多个线程获取读锁,同时访问同一个资源。
  • 排他锁:顾名思义该所具有排他性,只允许一个线程获取写锁,不允许同时访问同一个资源。

        加锁确实可以解决线程安全的问题,比如加上写锁的话,其实就相当于把并行任务变成串行任务,这样子就会带来并发性能问题。出现性能问题就需要相应改进锁并给出优化方案,Java中有相应的六种锁的优化机制。但是在开始梳理相应的锁优化方法前我们需要明确加锁到底意味着什么?为什么加锁会带来性能问题?

2.1 加锁本质理解 

        Java中加锁的本质就是不同的线程之间竞争同步状态,可以理解成一个访问资格或者是一个标记,如果一个线程竞争到了资源,那么这个标记就会被修改,其余的线程在竞争中就无法得到这个锁直至该线程释放锁资源。在Synchronized中通过操作系统层面的Mutex机制来实现同步状态,不同的线程之间通过竞争Mutex机制来实现互斥状态的处理。这个过程是通过调用一些内核指令来实现的,那么就不可避免涉及到用户态到内核态的切换,这个切换过程会占用CPU的资源并消耗性能。

切换过程消耗性能原因:

        第一个原因是:在用户态到内核态的切换过程中,用户线程会阻塞等待并切换到内核线程来运行,那么就涉及到当前用户态执行指令的上下文的保存以及切换到内核线程后的执行指令,这就涉及到了线程的阻塞唤醒以及上下文的保存,这部分的操作是比较消耗性能滴。

第二个原因是在线程层面,如果某一个线程抢到了锁资源,那么其余线程就会阻塞等待。

线程性能影响的体现:

  • 竞争同步状态时涉及上下文切换和保存;
  • 线程的阻塞和唤醒,会涉及到切换操作影响性能;
  • 并行到串行的改变  

2.2 锁的优化机制 

在影响线程性能的因素中,我们只能通过优化线程的阻塞和唤醒过程来减少切换操作以提高性能。 

2.2.1 锁的粒度优化

通过优化Java中持有锁的粒度大小来改变锁持有时间或者是锁的作用范围,从而减少阻塞和竞争,提高并发性能。在优化锁的粒度时,我们可以将锁的粒度细化到最小范围,即只锁定必要的共享资源,而不是整个对象或方法。Java中中的synchronized和ReenrantLock关键字可以实现细粒度锁。当然也可以通过使用分段锁和减少锁的持有时间来减少锁的竞争。

2.2.2 无锁化编程和乐观锁

        无锁化编程(Lock-Free Programming)是一种并发控制策略,它通过避免使用传统的锁(如互斥锁)来实现多线程之间的同步。在无锁化编程中,线程通过原子操作和其他无锁算法来操作共享资源,从而避免了线程间的阻塞和竞争。这意味着即使在高并发的情况下,线程不会因为等待锁而被阻塞,从而提高了并发程序的性能和可伸缩性。无锁化编程的实现通常依赖于硬件原子操作指令或特殊的数据结构(如CAS - Compare-and-Swap),它们允许线程在不使用传统锁的情况下进行原子性的数据修改。CAS是一种原子操作,用于实现多线程环境下的并发控制,它是无锁化编程中的关键机制之一。

缺点:无锁化编程可能会增加代码的复杂性,并且在复杂的情况下,可能会导致更多的线程冲突和错误。

        乐观锁通常基于CAS(Compare-and-Swap)操作实现。当线程想要更新共享资源时,它会先读取该资源的当前版本号或时间戳(作为期望值),然后通过CAS操作尝试将新值写入该资源(这里有一个比较的操作)。如果CAS操作成功,说明没有其他线程在此期间修改过该资源,更新成功。如果CAS操作失败,说明其他线程已经修改了资源,此时需要重新读取最新值并再次尝试更新,直到更新成功为止。

Tips:乐观锁主要是通过数据版本和时间戳来控制多线程并发数据修改的安全性。 

悲观锁

顾名思义,悲观锁与乐观锁相反,认为写多读少,遇到并发写的可能性高,每次去拿数据的时候都认为其他线程会修改,所以每次读写数据时都会上锁。其他线程想要读写这个数据时,会被这个线程挂起,直到这个线程释放锁然后再竞争锁资源。

2.2.3 偏向锁、重量级锁和轻量级锁(减少锁竞争)

自旋锁 

轻量级锁又称为自旋锁,顾名思义就是线程在阻塞前会执行一个循环过程一直请求锁资源访问锁的同步状态。 我们通过一个图来弄清以下这个过程:

在Thread2竞争到锁资源、给Thread1加上自旋锁后Thread1会在线程阻塞之前一直执行自选尝试来竞争锁资源,这样就可以减少线程的阻塞和唤醒次数,提高性能。

缺点: 占用处理器的时间,如果占用的时间很长,会白白消耗处理器资源,而不会做任何有价值的工作,带来性能的浪费。因此自旋等待的时间必须有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程。

自适应自旋

        自适应意味着自旋的时间不再是固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。有了自适应自旋,随着程序运行时间的增长及性能监控信息的不断完善,虚拟机对程序锁的状态预测就会越来越精准。 

偏向锁

如果给一个线程T1加上偏向锁,当该线程在第一获得锁资源的时候会把该锁偏向T1,在T1再次进入锁时就不需要再次同其它的线程进行竞争了。

重量级锁

可以简单理解重量级锁就是传统的锁模式, 正常进行大量的用户态到内核态的切换和线程阻塞。我们把这种依赖于操作系统 Mutex Lock来实现的锁称为重量级锁。

2.2.4 锁消除和锁膨胀

锁消除

锁消除其实是编译器或运行时系统在分析代码时,消除掉不会被线程竞争的共享数据的锁的过程。

锁膨胀

锁膨胀,或者叫做锁粗化,就是将多个连续的细粒度锁合并为一个粗粒度锁的优化过程。简单理解在一个循环中我们多层嵌套加锁或者多次进行锁操作会带来更大的性能消耗,因此采用将加锁的范围扩展到整个操作序列的外部的方式来优化,可以看做锁的粒度的增大。

2.2.5 读写锁优化 

读写锁的优化过程就是在读多写少的场景下,在读的场景写使用读锁,在写的场景下使用写锁。读锁与读锁之间不互斥、读锁与写锁之间互斥、写锁与写锁之间互斥。

2.2.6 公平锁和非公平锁

公平锁

        公平锁是指多个线程按照请求锁的顺序来获取锁。当多个线程同时竞争一个公平锁时,锁将按照它们的请求顺序,根据等待队列的排序来逐个分配给这些线程。如果有线程在等待获取锁,那么在锁释放时,等待时间最长的线程将有更高的优先级获得锁。这种方式可以保证锁的获取是按照公平的原则进行的,避免了线程饥饿的问题。

ReentrantLock fairLock = new ReentrantLock(true); // 创建公平锁
ReentrantLock fairLock = new ReentrantLock(false); // 创建非公平锁

非公平锁

        非公平锁是指多个线程尝试获取锁时,没有按照请求顺序来进行获取。在非公平锁中,当锁释放时,任何一个正在等待获取锁的线程都有机会获取到锁,即使其他线程已经在等待了较长时间。

  • 优点:非公平锁的性能会高于公平锁
  • 缺点:可能导致某些线程一直获取不到锁,出现线程饥饿的问题。  

2.3 锁的特性  

2.3.1 可重入锁

Java中大部分的锁都是可重入的。重入锁指的是一个线程抢占到一个锁资源并在释放锁之前再次竞争同一把锁的时候无需阻塞等待,直接获取锁资源即可。这种锁是通过递归的方式来实现的,所以又称为递归锁,主要作用就是避免死锁的情况出现。

2.3.2 分布式锁 

        分布式锁是一种在分布式系统中用于协调多个节点对共享资源进行访问的机制。在分布式环境下,由于多个节点之间的并发操作,需要一种可靠的方法来确保在同一时间只有一个节点能够获取到锁,从而保证共享资源的一致性和正确性。在前面中我们知道Java中可以使用synchronized关键字来为线程加锁,而分布式锁是为了解决在分布式架构下的锁的粒度的问题,面向的是进程维度。


三、Synchronized、Lock和ReentrantLock特点和区别 

3.1 Synchronized(关键字)

Synchronized是Java语言提供的内置锁机制,通过在方法或代码块上加上synchronized关键字来实现线程同步。它是一种隐式锁,由JVM自动管理锁的获取和释放

使用场景:适用于简单的线程同步场景,例如对单个方法或代码块进行同步控制。

特点:Synchronized具有自动加锁和释放锁的功能,但不支持公平锁的获取方式。

3.2 Lock(接口):

Lock是Java的并发API提供的接口,它提供了更灵活的线程同步机制,可以显式地控制锁的获取和释放,支持可重入锁、悲观锁、独占锁、互斥锁、同步锁。

使用场景:适用于复杂的线程同步场景,例如需要手动控制锁的获取和释放,或者需要使用公平锁等特定需求。

特点:Lock需要手动编写获取锁和释放锁的代码,并且支持公平锁和非公平锁两种获取方式。

3.3 ReentrantLock(类):

ReentrantLock是Lock接口的一个实现类,提供了可重入锁、悲观锁、独占锁、互斥锁、同步锁。

使用场景:适用于需要支持可重入性的场景,例如在一个方法中调用另一个方法时,可能需要多次获取同一个锁,同时需要手动开启和销毁锁。

特点:ReentrantLock提供了与Synchronized相似的功能,但更加灵活,可以支持公平锁和非公平锁,以及可中断等特性。

3.4 区别

  • Synchronized是Java语言提供的内置锁,Lock是Java的并发API提供的接口,ReentrantLock是Lock接口的一个实现类。
  • Synchronized是隐式锁,由JVM自动管理锁的获取和释放,而Lock和ReentrantLock需要显示的调用lock和unlock方法手动编写获取锁和释放锁的代码。
  • ReentrantLock支持可重入性,即同一个线程可以多次获取同一个锁,而Synchronized不支持。
  • Lock和ReentrantLock可以支持更多的高级特性,如公平锁、非公平锁、可中断锁等。而Synchronized只支持非公平锁。
  • 在性能方面,Synchronized在JDK 6以后进行了优化,性能较以前有了很大提升,但在高并发情况下,Lock和ReentrantLock通常比Synchronized更有优势。

 


总结

        荔枝通过一篇文章梳理了锁的创建需求以及由性能问题引出的一系列优化后的锁的概念,接着区分了Java中实现锁和线程异步的三种方法的特点和使用场景。总之梳理下来荔枝对于锁的脉络知识也算有了清晰的理解,具体细节的操作可能需要在实际的项目场景中取应用吧哈哈哈~~~希望小伙伴们读完后能有所收获哈哈哈哈,一起加油吧。

今朝已然成为过去,明日依然向往未来!我是小荔枝,在技术成长的路上与你相伴,码文不易,麻烦举起小爪爪点个赞吧哈哈哈~~~ 比心心♥~~~

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

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

相关文章

【CAS6.6源码解析】深度解析票据淘汰与过期策略-探究数据淘汰策略的设计

票据作为一种时效很敏感的数据,其过期策略的设计对其功能性和性能影响很大。本文将深度解析票据淘汰与过期策略,并基于此探究数据淘汰策略的设计,让我们一起走进企业级中央认证中心CAS的源码,分析其设计的巧妙之处。 文章重点分析…

二十三种设计模式第二十二篇--中介者模式

说到这个模式就有趣了,不知道大家在生活中喷到过中介没?其实中介这个词吧,我也说不上好还是坏,有时候他可以帮助人们更快的达到某个目的,但有的时候吧,这个有贼坑人,相信网络上有各种被中介坑的…

Redis场景应用:详细实现网站粉丝关注与展示的功能

🏆作者简介,黑夜开发者,全栈领域新星创作者✌,阿里云社区专家博主,2023年6月csdn上海赛道top4。多年电商行业从业经验,对系统架构,数据分析处理等大规模应用场景有丰富经验。 🏆本文…

嵌入式Linux下 i2c-tool工具的使用方法 包括i2cdetect、i2cget、i2cset、i2cdump、i2ctransfer

要想用Linux i2c-tools必须安装如下套件,安装后就可以使用i2cdetect、i2cdump、i2cset、i2cget、i2ctransfer了。 sudo apt install i2c-tools -yi2cdetect命令 该命令用于扫描I2C总线上的设备。 语法:i2cdetect [-y] [-a] [-q|-r] i2cbus [first las…

[论文笔记] chatgpt系列 2.6 DeepSpeed-chat 数据集

一、FT数据集 & Reward model数据集 Deepspeed-chat 源代码的数据集: Dahoas/rm-static: 这是一个用于强化学习的静态环境数据集,包含了一个机器人在一个固定环境中的运动轨迹。该数据集旨在用于评估强化学习算法在静态环境下的表现。 Dahoas/full-hh-rlhf: 这是一个用于…

二十三种设计模式第二十四篇--访问者模式(完结撒花)

在访问者模式(Visitor Pattern)中,我们使用了一个访问者类,它改变了元素类的执行算法。 通过这种方式,元素的执行算法可以随着访问者改变而改变。 这种类型的设计模式属于行为型模式。根据模式,元素对象已接…

openGauss学习笔记-27 openGauss 高级数据管理- JOIN

文章目录 openGauss学习笔记-27 openGauss 高级数据管理- JOIN27.1 交叉连接27.2 内连接27.3 左外连接27.4 右外连接27.5 全外连接 openGauss学习笔记-27 openGauss 高级数据管理- JOIN JOIN子句用于把来自两个或多个表的行结合起来,基于这些表之间的共同字段。 在…

SLA探活工具EaseProbe

工具介绍 EaseProbe可以做三种工作:探测、通知和报告。 项目地址:https://github.com/megaease/easeprobe 1、安装 [rootlocalhost ]# yum -y install unzip go [rootlocalhost ]# unzip easeprobe-main.zip [rootlocalhost ]# cd easeprobe-main [r…

如祺出行冲刺自动驾驶商业化,人少的地方机会多?

网约车,正在迎来让人“不明觉厉”的新一轮竞赛。 网约车监管信息交互系统的数据显示,截至今年6月30日,全国共有318家网约车平台公司取得网约车平台经营许可,环比增加5家;网约车监管信息交互系统6月份共收到订单信息7.…

作为新手小白,你应该了解的五个3DMAX的使用干货小技巧!

3Dmax是一款著名的三维建模和动画制作软件,广泛应用于建筑设计、影视特效、游戏开发等领域。对于初学者来说,熟练掌握一些干货小技巧,可以帮助大家更快地上手和使用这款强大的软件。 一、学习基础操作技巧 首先,你需要学习一些基…

留存测试数据,Apipost接口用例详解

接口用例可以在不影响源接口数据的情况下对接口添加多个用例,方便测试并保存测试数据。 创建用例 左侧目录选择接口后进入接口用例页面,点击添加用例 在弹出窗口中修改各种参数。如登录接口,可修改用户名为空,并添加断言。 执行…

【phaser微信抖音小游戏开发006】给文本增加点击事件

新建st006,为文本增加点击事件。 我们加了一个计数的count,点击一次增加一下,并显示到屏幕上去。 效果如下图: 其它的对象以此类推即可,先置inputEnable为true,然后再增加一个inputDown事件即可。

IO进程线程day4(2023.8.1)

一、Xmind整理: 进程的五态图: 内存分布图: 注:栈区:存储局部变量,形参(上边打错了!!!) 虚拟内存和物理内存: 进程的STAT&#xff1a…

C++设计模式之装饰者模式

文章目录 C装饰者设计模式什么是装饰者模式优缺点优点缺点 如何使用 C装饰者设计模式 什么是装饰者模式 装饰者模式是一种设计模式,它允许我们动态地将行为附加到对象上,而无需改变对象本身的定义。它将一个对象的行为包装在一个独立的的对象中&#xf…

数据库事务--数据库事务隔离级别实战

2、演示环境 数据库及工具 ➢MySQL版本 5.5.47 ➢数据库工具 Navicat for MySQL 数据库命令 ➢查看数据库版本: select version();➢查看数据库现在的隔离级别: select session.tx_ isolation;➢修改隔离级别: set session.tx_ _isolation级别参数;➢级别参数: READ-UN…

内网横向移动—非约束委派约束委派

内网横向移动—非约束委派&约束委派 1. 委派攻击介绍1.1. 约束委派分类 2. 非约束委派2.1. 配置非约束委派2.1.1. 域内主机配置2.1.2. 注册对象2.1.3. 域内用户配置 2.2. 案例测试2.2.1. 查询服务账户2.2.2. 查询机器账户2.2.3. 机器通讯2.2.4. 导出票据2.2.5. 导入票据2.2…

算法题--二叉树(二叉树的最近公共祖先、重建二叉树、二叉搜索树的后序遍历序列)

目录 二叉树 题目 二叉树的最近公共祖先 原题链接 解析 二叉搜索树的最近公共节点 核心思想 答案 重建二叉树 题目链接 解析 核心思想 答案 二叉搜索树的后序遍历序列 原题链接 解析 核心思想 答案 二叉树 该类题目的解决一般是通过节点的遍历去实现&#x…

edge://settings/defaultbrowser default ie

Microsoft Edge 中的 Internet Explorer 模式 有些网站专为与 Internet Explorer 一起使用,它们具有 Microsoft Edge 等新式浏览器不支持的功能。 如果你需要查看其中的某个网站,可使用 Microsoft Edge 中的 Internet Explorer 模式。 大多数网站在新…

优先级队列 (堆)

目录 一,堆的概念 二, 堆的存储结构 三, 堆的实现 3.1 shiftDown() 3.2 shiftUp() 3.3 shiftDown 与 shiftUp 的时间复杂度 四,堆排序 一,堆的概念 堆常用于实现优先队列(Priority Queue&#xff0…

【算法训练营】求最小公倍数+另类加法+走方格的方案数

7月31日 求最小公倍数题目题解代码 另类加法题目题解代码 走方格的方案数题目题解| 1 | 2 | 3 || 4 | 5 | 6 || 7 | 8 | 9 |代码 求最小公倍数 题目 点击跳转: 求最小公倍数 题解 最小公倍数 两数之积除以最大公约数,这里使用碾转相除法进行最大公约数的求解&am…