8分钟搞懂Java中的各种锁
转载请注明出处️
作者:测试蔡坨坨
原文链接:caituotuo.top/f9fc66cb.html
前言
你好,我是测试蔡坨坨。
在前几篇Redis相关文章中都说到了锁,同时我们在参加设计评审或者codeReview时也会接触到关于加锁的问题。因此,作为测试人员,还是很有必要搞懂相关的锁机制。
你是否背了很多关于锁的面试题,但还是没有搞懂锁到底有哪些东西,学了很多锁之后,发现越搞越模糊。
不要慌,本篇我们就来聊一聊Java中的各种锁。
什么是锁
说到锁,我们自然而然会想到Synchronized、Lock、Reentrantlock、分布式锁等很多锁的类型。
那么第一个问题,我们要搞清楚锁到底解决什么问题?
很简单,锁要解决的一个问题就是线程安全
问题。
所谓线程安全,主要体现在三方面:原子性、可见性和有序性。
- 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作。
- 可见性:一个线程对主内存的修改可以及时被其他线程看到。
- 有序性:一个线程观察其他线程的指令执行顺序,由于在JMM中允许编译器和处理器对指令重排序,因此该观察结果一般杂乱无序。
而Synchronized同步关键字是可以解决我们在多线程开发领域中涉及到的线程安全问题。
线程安全问题在实际开发中又是如何体现的呢?
举个简单的栗子,有一个int类型的i=0存在主内存中,有两个线程Thread1和Thread2同时执行一个i++操作,此时这个结果可能等于1,也可能等于2。为什么呢?
因为i++这个指令是非原子指令,i++在Java中是一条指令,但是最终转成底层的汇编指令是三条指令:
- 先从内存加载i的值(get)
- 对i进行递增(modify)
- 把i的值写回到内存中(set)
两个线程同时操作这三条指令时,就有可能两个线程同时拿到i,结果就是i=2。
所以,我们在这种场景中需要加排他锁,也叫同步锁。
这里的同步锁起到什么作用呢?
在没有加锁之前可能出现最终结果等于2的情况,是因为两个线程同时执行,同时拿到i的值,也就是并行操作,而加上同步锁就是让并行变成串行。
同步锁的特点就是多个线程访问共享资源时,在同一时刻只允许一个线程访问这个共享资源,这样就能够解决原子性问题。
功能层面
从功能层面来说,锁在Java并发编程中只有两类:共享锁
和排它锁
。
共享锁也叫读锁,读锁的特点是在同一时刻允许多个线程抢占到锁。
排它锁也叫写锁,写锁的特点是在同一时刻只允许一个线程抢占到锁。
性能和线程安全
我们经常听到的乐观/悲观锁、自旋锁、可重入锁、偏向锁、轻量/重量级锁又是什么呢?
这就需要从第二个维度进行拆分,加锁必然存在性能问题,因为加锁使得并行变成串行,并行的效率一定比串行高,加锁会造成阻塞。在软件开发中需要考虑两个点,性能和线程安全。例如:库存扣减,既要保证性能,又要保证线程的并发安全,保证原子性的修改。
在这个层面上来说,我们如何优化,如何权衡性能和线程安全两者之间的关系呢?
锁粒度的优化,把锁的范围缩小,保证锁竞争的范围在目标需求范围内就好了。
无锁化编程(乐观锁)/悲观锁
乐观锁是没有加锁的,它是通过一个数据的版本来控制多线程并发的数据修改安全性。
既然说到了乐观锁,不得不提一嘴悲观锁,乐观锁和悲观锁有什么区别呢?
举个栗子,小明同学去上厕所,厕所里有一万个坑位,悲观锁的场景就是虽然有一万个坑位,但是小明也担心有人会来跟他抢一个坑位,于是小明上去二话不说就加了把锁;乐观锁的场景就是一万个坑位就小明一个人,小明进去之后也不用上锁,也不担心有人会来抢同一个坑位。
偏向锁/轻量级锁(自旋锁)/重量级锁
加锁的本质实际上是去竞争一个同步状态,如上图所示,有一个文件,两个线程需要去竞争文件的访问资格,如何知道是否有访问资格呢?可以通过一个同步标识,比如int status=0/1(0表示空闲,1表示繁忙),线程1竞争到锁,进入访问时将status修改成1,线程2再进入到锁判断时,只需要去判断当前的status是否等于1即可。
Synchronized是通过操作系统层面的Mutex机制(mutually exclusive)实现同步状态,通过竞争Mutex机制,实现互斥状态的处理。线程1和2去竞争Mutex的时候,会涉及到内核指令的调用,因为Mutex是操作系统层面提供的一个互斥机制,所以需要通过内核指令去调用这个机制来实现互斥竞争行为。
这个地方就会涉及到
用户态
和内核态
的切换,这个切换会占用CPU资源,消耗性能,因为用户线程要进入阻塞等待,然后切换到内核线程来运行,需要把当前运行的线程执行指令的上下文保存起来,同时要切换到内核线程去执行指令,也就是会涉及到线程的阻塞
、唤醒
以及上下文的保存
。假设线程2竞争到了锁,线程1就会进入阻塞等待,所以加锁会影响性能。
性能主要体现在三个方面:
1.竞争同步状态时涉及到上下文切换,也就是从用户态到内核态的切换
2.线程阻塞和唤醒的切换
3.并行到串行的改变
由于1和3无法改变,所以我们重点关注第二点线程的阻塞和唤醒,这个地方的切换是否能够避免,也就是说线程2竞争到锁之后,线程1不去阻塞等待,也就是让线程1在阻塞之前进行重试(重试就是线程1第一次尝试加锁,发现线程2已经获取到锁,这时就进入下一次循环再进行重试)。这种方式也就是所谓的
自旋锁
,在阻塞等待之前通过一定的自旋尝试去竞争锁资源,也叫做轻量级锁
。咱就是说我们加锁的代码有没有可能压根就不存在竞争场景?有可能。
我们加锁的目的是保证这段代码的线程安全性,但是有可能在实际开发中这段代码压根就不存在竞争。
举个栗子,前端页面做了非空校验,理论上传给后端的参数就不会为空,但是也有可能有人直接调用接口传一个空值,所以后端一般都做非空校验,也叫做防御性编程。
同理,如果一段代码中
锁的竞争必要性不存在
,但是我们又想保护这段代码,于是就引入了偏向锁。所谓偏向锁就是当线程1进入锁的时候,如果当前不存在竞争,那么它就会把这个锁偏向线程1,线程1下次再进入的时候,就不再需要竞争锁。
简而言之,偏向锁可以认为没有竞争,轻量级锁存在轻微竞争,而重量级锁就是整个的实现。
锁消除/锁膨胀
在jdk中还引入了锁消除和锁膨胀,这是编译器层面的优化,主要优化加锁的性能。
锁消除也就是代码本身可能就没有线程安全问题,但是你又加了锁,然后jvm编译的时候发现这个地方加了锁,导致无效竞争,那么它就会把这个锁消除掉。锁膨胀是因为控制的锁粒度太小,导致频繁加锁和释放锁,所以它就会把锁的范围扩大。
读写锁
读写锁也是一种优化,读操作不会影响数据的准确性,因为它不会修改数据,也就是说读操作不需要加锁,针对读多写少的场景,读写锁可以确保读和读不会互斥,不需要竞争锁,而写和写实现互斥。
公平锁/非公平锁
公平锁就是每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的;非公平锁就是每个线程获取锁的顺序是随机的,并不会遵循先来后到的规则,所有线程会竞争获取锁。
默认情况下锁都是非公平的,比如Synchronized(只能为非公平锁)、Reentrantlock(在创建Reentrantlock时可以手动指定成公平锁),因为非公平锁的性能要比公平锁的性能更好,非公平锁意味着减少了锁的等待,减少了线程的阻塞和唤醒。
锁的特性
第三维度从锁的特性来说,又会有重入锁和分布式锁。
重入锁
锁主要用来控制多线程访问问题,对于同一线程,如果连续两次对同一把锁进行加锁,那么这个线程就会被卡死,在实际开发中,方法之间的调用错综复杂,一不小心就可能在多个不同的方法中反复调用lock(),造成死锁。
重入锁就是用来解决这个问题的,使得同一线程可以对同一把锁在不释放的前提下,反复加锁不会导致线程卡死,唯一的一点就是需要保证lock()和unlock()的次数相同。
分布式锁
分布式锁是解决分布式架构下粒度的问题,解决的是进程维度的问题,而Synchronized是解决Java并发里面的线程维度。关于分布式锁更多知识点后面我们单独来讨论。
8分钟搞懂Java中的各种锁的更多相关文章
- 轻松搞懂Java中的自旋锁
前言 在之前的文章<一文彻底搞懂面试中常问的各种“锁”>中介绍了Java中的各种“锁”,可能对于不是很了解这些概念的同学来说会觉得有点绕,所以我决定拆分出来,逐步详细的介绍一下这些锁的来龙 ...
- 一文彻底搞懂Java中的环境变量
一文搞懂Java环境变量 记得刚接触Java,第一件事就是配环境变量,作为一个初学者,只知道环境变量怎样配,在加上各种IDE使我们能方便的开发,而忽略了其本质的东西,只知其然不知其所以然,随着不断的深 ...
- 一文带你看懂Java中的Lock锁底层AQS到底是如何实现的
前言 相信大家对Java中的Lock锁应该不会陌生,比如ReentrantLock,锁主要是用来解决解决多线程运行访问共享资源时的线程安全问题.那你是不是很好奇,这些Lock锁api是如何实现的呢?本 ...
- 一文带你搞懂java中的变量的定义是什么意思
前言 在之前的文章中,壹哥给大家讲解了Java的第一个案例HelloWorld,并详细给大家介绍了Java的标识符,而且现在我们也已经知道该使用什么样的工具进行Java开发.那么接下来,壹哥会集中精力 ...
- 一文搞懂--Java中重写equals方法为什么要重写hashcode方法?
Java中重写equals方法为什么要重写hashcode方法? 直接看下面的例子: 首先我们只重写equals()方法 public class Test { public static void ...
- 一文搞懂 Java 中的枚举,写得非常好!
知识点 概念 enum的全称为 enumeration, 是 JDK 1.5 中引入的新特性. 在Java中,被 enum关键字修饰的类型就是枚举类型.形式如下: enum Color { RED, ...
- 来吧,一文彻底搞懂Java中最特殊的存在——null
没事的时候,我并不喜欢逛 P 站,而喜欢逛 programcreek 这些技术型网站,于是那天晚上,在夜深人静的时候,我就发现了一个专注基础但不容忽视的主题.比如说:Java 中的 null 到底是什 ...
- 一篇文章让你搞懂Java中的静态代理和动态代理
什么是代理模式 代理模式是常用的java设计模式,在Java中我们通常会通过new一个对象再调用其对应的方法来访问我们需要的服务.代理模式则是通过创建代理类(proxy)的方式间接地来访问我们需要的服 ...
- 彻底搞懂Java中equals和==的区别
java当中的数据类型和“==”的含义: 1.基本数据类型(也称原始数据类型) :byte,short,char,int,long,float,double,boolean.他们之间的比较,应用双等号 ...
- 来吧,一文彻底搞懂Java中的Comparable和Comparator
大家好,我是沉默王二,今天在逛 programcreek 的时候,我发现了一些专注细节但价值连城的主题.比如说:Java 的 Comparable 和 Comparator 是兄弟俩吗?像这类灵魂拷问 ...
随机推荐
- cmake安装及报错解决办法
安装 yum install cmake 报错 centOS8(x86_64 或 aarch64) 系统下 yum或dnf 默认安装的 cmake-3.18.2-11.el8版本,安装后无法使用,出现 ...
- docker安装mysql服务
拉取镜像 docker pull mysql:5.7.3 运行镜像 docker run -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7.32 # -e MYSQL_ ...
- 使用objc4V818.2源码编译,没有什么比苹果底层源码更有说服力去证明底层原理真假
前言 为什么会想要调试源码? 苹果开源了部分源码, 但相似内容太多, 基本找不到代码见的对应关系, 如果能像自己工程一样进行跳转那多好哇~~ 苹果源码开源地址: https://opensource. ...
- 【Azure Cloud Service】Cloud Service(Classic) 迁移失败,找不到解决方案怎么办?
问题描述 很老很老的云服务,在迁移到 Cloud Service(Extended Support)[云服务外延支持] 时,迁移的验证步骤不通过,因为资源中没有包含虚拟网络(Virtual Netwo ...
- Jmeter 响应断言你知道多少?
1 断言各组件介绍 Apply to:同上 测试字段: * 响应文本:响应体 * 响应代码:响应状态码 * 响应信息:状态码的消息 * 响应头:顾名思义就是响应头 * 请求头:顾名思义就是请求头 * ...
- RocketMQ(10) 消息类型
一.普通消息 1. 消息发送方式分类 Producer对于消息的发送方式也有多种选择,不同的方式会产生不同的系统效果. 同步发送消息: 同步发送消息是指,Producer发出⼀条消息后,会在收到MQ返 ...
- Java 多线程---线程优先级
Java 实例 - 线程优先级设置 以下实例演示了如何通过setPriority() 方法来设置线程的优先级: 1 SimplePriorities.java 文件 2 public class Si ...
- 专访实在智能孙林君:颠覆传统RPA的实在IPA模式如何做到真正人人可用?
王吉伟对话实在智能孙林君:颠覆传统引领RPA行业的实在IPA模式是如何炼成的? 王吉伟对话实在智能孙林君:为什么第一款颠覆行业的RPA诞生在实在智能? 专访实在智能孙林君:打造出真正人人可用的实在 ...
- 快速带你入门css
css复习笔记 1. css样式值 1.1 文字样式 1 p{ 2 font-size: 30px;/*设置文字大小*/ 3 font-weight: bold;/*文字加粗*/ 4 font-sty ...
- 摆脱鼠标系列 - vscode 软件 最大化快捷键 - win + ↑
摆脱鼠标系列 - vscode 软件 最大化快捷键 - win + ↑ vscode默认打开不是最大化,所以按 win + 上箭头 使其最大化 不想按 F11 那个不太方便,左上角就没有项目名称了 优 ...