Java 中的全部锁

目录

一. 前言

二. 乐观锁 VS 悲观锁

三. 自旋锁 VS 适应性自旋锁

四. 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁

五. 公平锁 VS 非公平锁

六. 可重入锁 VS 非可重入锁

七. 独享锁(排他锁) VS 共享锁

八. 总结


一. 前言

    Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。本文旨在对锁相关源码(本文中的源码来自JDK 8和Netty 3.10.6)、使用场景进行举例,为读者介绍主流锁的知识点,以及不同的锁的适用场景。

    Java中往往是按照是否含有某一特性来定义锁,我们通过特性将锁进行分组归类,再使用对比的方式进行介绍,帮助大家更快捷的理解相关知识。下面给出本文内容的总体分类目录:

二. 乐观锁 VS 悲观锁

    乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度,在Java和数据库中都有此概念对应的实际应用。

    先说概念,对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。

    而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。

    乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

根据从上面的概念描述我们可以发现:
悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。 

光说概念有些抽象,我们来看下乐观锁和悲观锁的调用方式示例:

// ------------------------- 悲观锁的调用方式 -------------------------
// synchronized
public synchronized void testMethod() {
	// 操作同步资源
}
// ReentrantLock
private ReentrantLock lock = new ReentrantLock(); // 需要保证多个线程使用的是同一个锁
public void modifyPublicResources() {
	lock.lock();
	// 操作同步资源
	lock.unlock();
}

// ------------------------- 乐观锁的调用方式 -------------------------
private AtomicInteger atomicInteger = new AtomicInteger();  // 需要保证多个线程使用的是同一个AtomicInteger
atomicInteger.incrementAndGet(); //执行自增1

通过调用方式示例,我们可以发现悲观锁基本都是在显式的锁定之后再操作同步资源,而乐观锁则直接去操作同步资源。那么,为何乐观锁能够做到不锁定同步资源也可以正确的实现线程同步呢?具体可以参考JUC原子类: CAS, Unsafe和原子类。

三. 自旋锁 VS 适应性自旋锁

    在介绍自旋锁前,我们需要介绍一些前提知识来帮助大家明白自旋锁的概念。阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

    在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。

    而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。

自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

四. 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁

    这四种锁是指锁的状态,专门针对synchronized的。在介绍这四种锁状态之前还需要介绍一些额外的知识。

    总结而言:偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。

五. 公平锁 VS 非公平锁

    公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

    非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

直接用语言描述可能有点抽象,这里作者用从别处看到的一个例子来讲述一下公平锁和非公平锁:

如上图所示,假设有一口水井,有管理员看守,管理员有一把锁,只有拿到锁的人才能够打水,打完水要把锁还给管理员。每个过来打水的人都要管理员的允许并拿到锁之后才能去打水,如果前面有人正在打水,那么这个想要打水的人就必须排队。管理员会查看下一个要去打水的人是不是队伍里排最前面的人,如果是的话,才会给你锁让你去打水;如果你不是排第一的人,就必须去队尾排队,这就是公平锁。 

但是对于非公平锁,管理员对打水的人没有要求。即使等待队伍里有排队等待的人,但如果在上一个人刚打完水把锁还给管理员而且管理员还没有允许等待队伍里下一个人去打水时,刚好来了一个插队的人,这个插队的人是可以直接从管理员那里拿到锁去打水,不需要排队,原本排队等待的人只能继续等待。如下图所示:

六. 可重入锁 VS 非可重入锁

    可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。下面用示例代码来进行分析:

public class Widget {
    public synchronized void doSomething() {
        System.out.println("方法1执行...");
        doOthers();
    }

    public synchronized void doOthers() {
        System.out.println("方法2执行...");
    }
}

在上面的代码中,类中的两个方法都是被内置锁synchronized修饰的,doSomething() 方法中调用doOthers() 方法。因为内置锁是可重入的,所以同一个线程在调用 doOthers() 时可以直接获得当前对象的锁,进入 doOthers() 进行操作。

如果是一个不可重入锁,那么当前线程在调用 doOthers() 之前需要将执行 doSomething() 时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。

为什么可重入锁就可以在嵌套调用时可以自动获得锁呢?我们通过图示和源码来分别解析一下:

还是打水的例子,有多个人在排队打水,此时管理员允许锁和同一个人的多个水桶绑定。这个人用多个水桶打水时,第一个水桶和锁绑定并打完水之后,第二个水桶也可以直接和锁绑定并开始打水,所有的水桶都打完水之后打水人才会将锁还给管理员。这个人的所有打水流程都能够成功执行,后续等待的人也能够打到水。这就是可重入锁。

但如果是非可重入锁的话,此时管理员只允许锁和同一个人的一个水桶绑定。第一个水桶和锁绑定打完水之后并不会释放锁,导致第二个水桶不能和锁绑定也无法打水。当前线程出现死锁,整个等待队列中的所有线程都无法被唤醒。 

之前我们说过ReentrantLock和synchronized都是重入锁,那么我们通过重入锁ReentrantLock以及非可重入锁NonReentrantLock的源码来对比分析一下为什么非可重入锁在重复调用同步资源时会出现死锁。 

首先,ReentrantLock和NonReentrantLock都继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0。

当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁。而非可重入锁是直接去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞。

释放锁时,可重入锁同样先获取当前status的值,在当前线程是持有锁的线程的前提下。如果status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁。而非可重入锁则是在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放。

七. 独享锁(排他锁) VS 共享锁

    独享锁和共享锁同样是一种概念。我们先介绍一下具体的概念,然后通过ReentrantLock和ReentrantReadWriteLock的源码来介绍独享锁和共享锁。

    独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。

    共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。

    独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

下图为ReentrantReadWriteLock的部分源码:

我们看到ReentrantReadWriteLock有两把锁:ReadLock和WriteLock,由词知意,一个读锁一个写锁,合称“读写锁”。再进一步观察可以发现ReadLock和WriteLock是靠内部类Sync实现的锁。Sync是AQS的一个子类,这种结构在CountDownLatch、ReentrantLock、Semaphore里面也都存在。 

在ReentrantReadWriteLock里面,读锁和写锁的锁主体都是Sync,但读锁和写锁的加锁方式不一样。读锁是共享锁写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升。

八. 总结

    本文Java中常用的锁以及常见的锁的概念进行了基本介绍,并从源码以及实际应用的角度进行了对比分析。限于篇幅,没有在本篇文章中对所有内容进行深层次的讲解。

    其实Java本身已经对锁本身进行了良好的封装,降低了开发同学在平时工作中的使用难度。但是开发同学也需要熟悉锁的底层原理,不同场景下选择最适合的锁。而且源码中的思路都是非常好的思路,也是值得大家去学习和借鉴的。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

中小型企业内网搭建

某中小型公司客户提出网络比较单一整体都在一个大的广播域中,AP无线的SSID有很多个,包括一些小的无线路由器散发出来的信号,用起来网络不太稳定,并且AP的SSID要分开,办公室只有单个SSID,不允许出现其他的&a…

vue3+vite+ts 发布自定义组件到npm

vue3vite 发布自定义组件到npm 初始化项目编写组件配置打包组件上传到npm测试组件库 初始化项目 // 创建项目 pnpm create vite vue-test-app --template vue-ts// 运行项目 cd vite vue-test-app pnpm install pnpm run dev编写组件 1、根目录下创建packages目录作为组件的开…

leetcode刷题日志-151反转字符串中的单词

给你一个字符串 s ,请你反转字符串中 单词 的顺序。 单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。 返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。 注意:输入字符串 s中可能会存在前导空格、尾随…

面向对象成员之属性

属性:通过方法改造出来 # 1.编写时 # 方法上方写property # 方法参数:只有一个self # 2.使用时:无需加括号 对象.方法 # 3.应用场景:对于简单的方法,当无需传参且有返回值时,可以使用 property class Foo(object):def _init_(self):...propertydef start(self):return 1pr…

【vitis】 AIE basic

AIE vs AIE-ML versal 期间分类 文件 操作 vitis -new -w . 安装

易点易动固定资产管理系统场景应用一:集成ERP/财务系统

在企业的日常运营中,固定资产管理是一个重要而繁琐的任务。传统的手工管理方式往往效率低下且容易出错,给企业带来不必要的成本和风险。为了解决这一问题,易点易动固定资产管理系统应运而生。本文将重点介绍易点易动固定资产管理系统在集成ER…

【C语法学习】26 - strcat()函数

文章目录 1 函数原型2 参数3 返回值4 使用说明5 示例5.1 示例1 1 函数原型 strcat():将src指向的字符串拼接在dest指向的字符串末尾,函数原型如下: char *strcat(char *dest, const char *src);2 参数 strcat()函数有两个参数src和dest&am…

SAP BAPI BAPI_CLASS_EXISTENCECHECK

物料分类是否存在的BAPI

Molecular Plant | ChIP-seq+RNA-seq解析E2F转录因子在植物复制胁迫响应中的独特和互补作用

生物体的生存完全依赖于它们对基因组完整性的维持,而基因组完整性受到增殖细胞复制胁迫的永久威胁。尽管植物DNA损伤反应(DDR)调节因子SOG1已被证明能够应对复制缺陷,但越来越多的证据表明,还有其他途径独立于SOG1发挥…

【IDEA 使用easyAPI、easyYapi、Apifox helper等插件时,导出接口文档缺少代码字段注释的相关内容、校验规则的解决方法】

问题 IDEA 使用easyAPI、easyYapi、Apifox helper等插件时,导出的接口文档上面,缺少我们代码里的注解字段,如我们规定了NOTNULL、字段描述等。 问题链接,几个月之前碰到过,并提问了,到现在解决&#xff0c…

k8s ingress高级用法一

前面的文章中,我们讲述了ingress的基础应用,接下来继续讲解ingress的一些高级用法 一、ingress限流 在实际的生产环境中,有时间我们需要对服务进行限流,避免单位时间内访问次数过多,常用的一些限流的参数如下&#x…

【Android】如何使用模拟器调试安卓项目

1、电脑安装逍遥模拟器,用来跑安卓项目。安装好模拟器之后,直接起安卓项目,自动会在选择设备处显示 2、如果前端是安卓后端是其他语言的话,这种前后端分离的模式,需要监听端口,原因是运行安卓和后端编译器都…

设计模式-11-模板模式

经典的设计模式有23种,但是常用的设计模式一般情况下不会到一半,我们就针对一些常用的设计模式进行一些详细的讲解和分析,方便大家更加容易理解和使用设计模式。 1-什么是模板模式 模板模式,全称是模板方法设计模式,英…

spark的资源调度与任务调度

blockManager 资源调度与任务调度

LangChain:打造自己的LLM应用

文章来源:https://www.cnblogs.com/jingdongkeji/p/17599838.html 1、LangChain是什么 LangChain是一个框架,用于开发由LLM驱动的应用程序。可以简单认为是LLM领域的Spring,以及开源版的ChatGPT插件系统。核心的2个功能为: 1&am…

国鑫受邀出席2023英特尔中国区数据中心渠道客户金秋会

10月18日,2023英特尔中国区数据中心渠道客户金秋会在重庆隆重举行,本次会议共邀请了全国英特尔数据中心渠道生态伙伴、本地服务器系统设计及制造厂商参与,Gooxi作为英特尔在中国区重要合作伙伴受邀参加。 会上,英特尔向生态合作伙…

玩具、儿童用品、儿童服装上亚马逊TEMU平台CPC认证办理

CPC认证是Childrens Product Certificate的简称,即儿童产品证书。它是美国强制性法规CPSIA要求的一部分,该法规主要针对12岁及以下儿童使用的产品,如玩具、儿童用品、儿童服装等。 一、儿童小汽车CPC测试项目可能会因产品标准和法规的不同而…

HIKVISION iSecure Center RCE 海康威视综合安防管理平台任意文件上传 POCEXP

参考:GitHub - Sweelg/HIKVISION_iSecure_Center-RCE: HIKVISION iSecure Center RCE 海康威视综合安防管理平台任意文件上传 POC&EXP(一键getshell) 速修!海康威视综合安防RCE已被用于勒索 近日,勒索团伙利用海康威视综合安防…

navigator.geolocation.getCurrentPosition在谷歌浏览器不执行的问题

/*** 获取我的位置*/getNavigatorLocation: function () {navigator.geolocation.getCurrentPosition(function (success) {console.log(inner>>>, success);if (success && success.coords) {var data success.coords;var point "POINT(" data.…

MySQL索引与事务

索引 概念 索引是一种特殊的文件,包含着对数据表里所有记录的引用指针.可以对表中一列或多列创建索引并指定索引的类型,各类索引有各自的数据结构的实现. 作用 数据库中的表,数据,索引之间的关系,类似于书架上的图书,书籍内容和书籍目录之间的关系. 索引所起的作用类似于书籍…