[多线程]一篇文章带你看懂Java中的synchronized关键字(线程安全)锁的深入理解

目录

1.前言

 2.synchronized的特性

2.1synchronized前言

2.2乐观锁和悲观锁

2.3重量级锁和轻量级锁

重量级锁 :

轻量级锁:

2.4自旋锁和挂起等待锁

2.5 公平锁和非公平锁

公平锁:

非公平锁:

2.6可重入锁和不可重入锁

可重入锁

不可重入锁:

2.7读写锁

3.sychronized原理和特点

1) 偏向锁

2) 轻量级锁

3) 重量级锁


1.前言

  我们都知道在多线程编程中,线程安全问题是很严重的问题。为了解决线程安全问题,我们引入了“锁这个概念”,Java中的锁是用snychrnized关键字来实现的,它是一种基于对象的锁。虽然在日常编程中,我们可以直接使用这个关键字,而不去考虑它内部的机制。但是常言道,朝闻道,夕死足以。在学习过程中我们更应该去庖丁解牛的深入理解它,而不是不求甚解。本篇文章,作者将带领大家重新认识sychrnized关键字,以及各种锁背后的机制和原因。

 2.synchronized的特性

2.1synchronized前言

  虽然在Java中,我们只需要使用一个简单的synchronzied来实现锁,但是它的内部的实现却不仅仅只是个简单的锁,是一个很复杂的过程。以下我们要讲的特性,主要是来给锁的实现者来实现的。普通的程序员也需要了解一下,可以让我们更深刻的理解锁这个概念。

2.2乐观锁和悲观锁

  悲观锁:总是假设最坏的情况,每次拿数据的时候都会觉得别人会把它修改,所以在每次拿数据的时候都会上锁,这样别人想拿到这个数据就会阻塞直到它拿到锁。

乐观锁:假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则返回用户错误的信息,让用户决定如何去做。

sychronized一开始使用乐观锁策略,当发现锁竞争比较频繁的时候,就会自动切换成悲观锁策略。

乐观锁有一个重要功能就是检测出数据是否发生访问冲突,我们可以引入一个“版本号“来解决

假设我们要修改多线程"用户账户余额“

假设账户余额为100,版本号初始为1,并且我们规定,提交版本必须大于当前版本才能执行更新余额。

1)线程A此时准备将其独出(versio=1,balance = 100),线程B这时也读入此信息

2)线程A操作将账户余额扣50,线程B扣20

3)线程A和线程B完成修改操作,都将版本号改为2,此时线程A(versio=2,balance = 50),线程B(versio=2,balance = 80)

4)这时候,线程A从把操作完成,然后去写入内存。(此时线程A的版本号为2,可以成功写入 内存中的数据为线程A 此时修改过的数据versio=2,balance = 50),线程B再去写入的时候,版本号也是2,并没有大于内存中的版本号,所以并没有成功。

不满足 “提交版本必须大于记录当前版本才能执行更新“ 的乐观锁策略。就认为这次操作失败

2.3重量级锁和轻量级锁
 

  锁的核心特性 "原子性", 这样的机制追根溯源是 CPU 这样的硬件设备提供的.
CPU 提供了 "原子操作指令".
 操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁.
 JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类.

synchornized 不仅仅是对mutex进行封装,在内部还做了很多其它的工资。

重量级锁 :

加锁机制重度依赖了 OS 提供了 mutex
大量的内核态用户态切换
很容易引发线程的调度
这两个操作, 成本比较高. 一旦涉及到用户态和内核态的切换, 就意味着 "沧海桑田".

轻量级锁:

加锁机制尽可能不使用 mutex, 而是尽量在用户态代码完成. 实在搞不定了, 再使用 mutex.
少量的内核态用户态切换.
不太容易引发线程调度

sychronized开始是一个轻量级锁,如果锁冲突比较严重,就会变成重量级锁。

2.4自旋锁和挂起等待锁

按之前的方式,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度.
但实际上, 大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU. 这个时候就可以使用自旋锁来处理这样的问题.

自旋锁就是没抢到锁,然后一直在cpu的处理下,尝试抢锁。

伪代码:
while(抢锁(lock)== 失败{


}

如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来.
一旦锁被其他线程释放, 就能第一时间获取到锁.

而挂起等待锁,就是陷入沉睡。等待被唤醒,并不像自旋锁一样,一直在尝试获取锁。

自旋锁是一种典型的轻量级锁实现方式:

优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁.
缺点: 如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源. (而挂起等待的时候是
不消耗 CPU 的)
synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的.
 

2.5 公平锁和非公平锁

假设三个线程,ABC,A先尝试获取,获取成功。然后B在尝试获取,获取失败,阻塞等待。,然后C也尝试获取,获取失败阻塞等待。

公平锁:

 遵循先来后到原则,B比C先来,当A 释放锁以后,B就能先C获取到锁

非公平锁:

并没有这种先来后到的原则,而是随即调度。

操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是非公平锁. 如果要
想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序.公平锁和非公平锁没有好坏之分, 关键还是看适用场景.

 

2.6可重入锁和不可重入锁

可重入锁

顾名思义。就是一个线程可以重复获取同一个锁多次,然后在依次释放。但是sychornized内部并不是真的加了很多把锁,而是通过计数器,如果加锁则计数器+1,如果释放锁则计数器-1。当计数器为0的时候,就会彻底释放锁。

不可重入锁:

一把锁只能同时被一个线程,拥有一次。

Linux系统提供的mutex锁是不可重入锁。

sychornizerd是可重入锁。

2.7读写锁

      在多线程里面。数据的读取之间是不会产生线程安全问题的,但是数据的写入会产生。数据写入和读者之间都需要互斥。如果两个场景用一把锁,会也很大的开销。为了这种常见的应用场景,所以Java引入了读写锁(reders-writer lock)

       一个线程对于数据的访问,有读和写两种操作:
如果都是读操作,那么就没有线程安全问题,直接并发读取就行。

如果都要写一个数据,就会有这个线程安全问题。

如果一个读一个写,也会有。

总结:写操作的时候会有线程安全问题。

其中:
读加锁和读加锁不互斥。

写加锁和写加锁互斥。

读加锁和写加锁互斥。

只要是涉及到 "互斥", 就会产生线程的挂起等待. 一旦线程挂起, 再次被唤醒就不知道隔了多
久了.
因此尽可能减少 "互斥" 的机会, 就是提高效率的重要途径
 

      Java标准库中,ReentranReadwritelock类,实现了读写锁。

ReetranReadwrite.ReadLock表示一个读锁,这个对象提供了lock和unlock方法进行加锁和解锁。

ReentranReawritelock.writeLock 这个类表示一个写锁,也提供了lock和unlock方法进行加锁和解锁。

读写锁特别适合于 "频繁读, 不频繁写" 的场景中
Synchronized 不是读写锁.
 

3.sychronized原理和特点

结合上面的锁策略,我们可以总结出,synchronized有以下特性:

1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
3. 实现轻量级锁的时候大概率用到的自旋锁策略
4. 是一种不公平锁
5. 是一种可重入锁
6. 不是读写锁
 

加锁过程:
JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级
 


 

1) 偏向锁


第一个尝试加锁的线程, 优先进入偏向锁状态.
偏向锁不是真的 "加锁", 只是给对象头中做一个 "偏向锁的标记", 记录这个锁属于哪个线程.
如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)
如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别
当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态.
偏向锁本质上相当于 "延迟加锁" . 能不加锁就不加锁, 尽量来避免不必要的加锁开销.
但是该做的标记还是得做的, 否则无法区分何时需要真正加锁.


2) 轻量级锁


随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁).
此处的轻量级锁就是通过 CAS 来实现.
通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
如果更新成功, 则认为加锁成功
如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).
 

3) 重量级锁


如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁
此处的重量级锁就是指用到内核提供的 mutex .
执行加锁操作, 先进入内核态.
在内核态判定当前锁是否已经被占用
如果该锁没有占用, 则加锁成功, 并切换回用户态.
如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.
经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒
这个线程, 尝试重新获取锁.
其他的优化操作
锁消除
编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除.
什么是 "锁消除
有些应用程序的代码中, 用到了 synchronized, 但其实没有在多线程环境下. (例如 StringBuffer)

StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");

此时每个 append 的调用都会涉及加锁和解锁. 但如果只是在单线程中执行这个代码, 那么这些加
锁解锁操作是没有必要的, 白白浪费了一些资源开销
 

 锁粗化
一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化.

实际开发过程中, 使用细粒度锁, 是期望释放锁的时候其他线程能使用锁.
但是实际上可能并没有其他线程来抢占这个锁. 这种情况 JVM 就会自动把锁粗化, 避免频繁申请释
放锁.
 

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

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

相关文章

bat 脚本的常用特殊符号

1、 命令行回显屏蔽符 2、% 批处理变量引导符 3、> 重定向符 4、>> 重定向符 5、<、>&、<& 重定向符 6、| 命令管道符 7、^ 转义字符 8、& 组合命令 9、&& 组合命令 10、|| 组合命令 11、"" 字符串界定符 12、, 逗号…

报错“找不到mfc100u.dll,程序无法继续执行”的解决方法,完美解决

在软件操作过程中&#xff0c;部分用户可能遇到"计算机缺失mfc140u.dll导致无法启动程序"的困扰。这种情况常常发生在启动某特定应用&#xff0c;特别是需要VC Redistributable支持的软件时。以下为详尽解决策略&#xff0c;让用户轻松应对这类技术难题&#xff0c;重…

React中的setState执行机制

我这里今天下雨了&#xff0c;温度一下从昨天的22度降到今天的6度&#xff0c;家里和学校已经下了几天雪了&#xff0c;还是想去玩一下的&#xff0c;哈哈&#xff0c;只能在图片里看到了。 一. setState是什么 它是React组件中用于更新状态的方法。它是类组件中的方法&#x…

人工智能与天文:技术前沿与未来展望

人工智能与天文&#xff1a;技术前沿与未来展望 一、引言 随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;在各个领域的应用越来越广泛。在天文领域&#xff0c;AI也发挥着越来越重要的作用。本文将探讨人工智能与天文学的结合&#xff0c;以及这种结合带…

SpringBoot之JSON参数,路径参数的详细解析

1.6 JSON参数 在学习前端技术时&#xff0c;我们有讲到过JSON&#xff0c;而在前后端进行交互时&#xff0c;如果是比较复杂的参数&#xff0c;前后端通过会使用JSON格式的数据进行传输。 &#xff08;JSON是开发中最常用的前后端数据交互方式&#xff09; 我们学习JSON格式参…

在VS2010上使用C#调用非托管C++生成的DLL文件(图文讲解)

背景 在项目过程中&#xff0c;有时候你需要调用非C#编写的DLL文件&#xff0c;尤其在使用一些第三方通讯组件的时候&#xff0c;通过C#来开发应用软件时&#xff0c;就需要利用DllImport特性进行方法调用。本篇文章将引导你快速理解这个调用的过程。 步骤 1. 创建一个CSharp…

散点图,盒须图,折线图混放在一个echarts

散点图&#xff0c;何须图&#xff0c;折线图混放在一个echarts option {tooltip: {trigger: axis,axisPointer: {type: cross,crossStyle: {color: #999}}},legend: {data:[盒须图1,盒须图2,折线图,散点图]},xAxis: [{type: category,data: [周一,周二,周三,周四,周五,周六…

Win11 TensorRT环境部署

一、CUDA和CUDNN安装 cuda和cudnn网上有很多安装教程&#xff0c;这里列举了一些&#xff0c;就不详细说了&#xff0c;具体链接如下&#xff1a; csdn.net - CUDA安装教程&#xff08;超详细&#xff09; 原创 zhihu.com - 深度学习之CUDACUDNN详细安装教程 tencent.com - C…

Sqoop安装与配置-shell脚本一键安装配置

文章目录 前言一、使用shell脚本一键安装1. 复制脚本2. 增加执行权限3. 执行脚本4. 加载用户环境变量5. 查看是否安装成功 总结 前言 本文介绍了如何使用Shell脚本一键安装Sqoop。Sqoop是一个用于在Apache Hadoop和结构化数据存储&#xff08;如关系数据库&#xff09;之间传输…

优先考虑泛型

Java中的泛型&#xff08;Generics&#xff09;提供了一种参数化类型的机制&#xff0c;使得你可以编写更灵活、类型安全的代码。下面是一个例子&#xff0c;说明在Java中优先考虑泛型的好处&#xff1a; 考虑一个简单的容器类&#xff0c;它可以存储任意类型的元素&#xff0…

【分享】WinRAR解压缩软件的超详细使用攻略

WinRAR是一款常见的解压缩软件&#xff0c;它使用方便&#xff0c;界面友好&#xff0c;在压缩率和速度方面都有很好的表现。 除了解压缩功能&#xff0c;WinRAR还有很多好用的功能&#xff0c;今天小编就来分享一下&#xff0c;WinRAR几个常用功能的具体使用方法。 1. 解压缩…

Axure之动态面板轮播图

目录 一.介绍 二.好处 三.动态面板轮播图 四.动态面板多方式登录 五.ERP登录 六.ERP的左侧菜单栏 七.ERP的公告栏 今天就到这了哦&#xff01;&#xff01;&#xff01;希望能帮到你了哦&#xff01;&#xff01;&#xff01; 一.介绍 Axure中的动态面板是一个非常有用的组…

可观测性是什么?新手入门指南!

如果您之前对可观测性重要性&#xff0c;益处&#xff0c;以及组成不甚了解&#xff0c;本文是一个合适的指南手册。 什么是可观测性&#xff1f; 可观测性被定义为根据系统产生的输出数据&#xff08;如日志&#xff0c;指标和链路追踪&#xff09;来衡量当前系统运行状态的…

Python学习开发mock接口

#1.测试为什么要开发接口&#xff1f; 1)在别的接口没有开发好的时候, mock接口(模拟接口) 2)查看数据, 避免直接操作数据库 #2.开发接口的顺序 1)安装flask flask是一个轻量级开发框架 pip install flask 2)开发一个接口 开发步骤&#xff1a; 1.实例化一个服务server:f…

【MySQL学习之基础篇】多表查询

文章目录 1. 多表关系1.1. 一对多1.2. 多对多1.3. 一对一 2. 多表查询概述2.1. 数据准备2.2. 概述 3. 查询的分类3.1. 内连接查询3.2. 外连接查询3.3. 自连接3.3.1. 自连接查询3.3.2. 联合查询 3.4. 子查询3.4.1. 概述3.4.2. 标量子查询3.4.3. 列子查询3.4.4. 行子查询3.4.5. 表…

gRPC框架

1、gRPC 与 Protobuf 介绍 微服务架构中&#xff0c;由于每个服务对应的代码库是独立运行的&#xff0c;无法直接调用&#xff0c;彼此间 的通信就是个大问题gRPC 可以实现微服务&#xff0c; 将大的项目拆分为多个小且独立的业务模块&#xff0c; 也就是服务&#xff0c; 各服…

[pluginviteimport-analysis] vite 提示jsx语法报错

参考文章 https://segmentfault.com/q/1010000043499356https://blog.csdn.net/kkkkkkgg/article/details/131168224 报错内容 内容类似如下&#xff1a; 03:16:26 [vite] Internal server error: Failed to parse source for import analysis because the content contains…

C语言刷题数组------数组交换

输入一个长度为 10的整数数组 X[10]&#xff0c;将里面的非正整数全部替换为 1&#xff0c;输出替换完成后的数组。 输入格式 输入包含 10个整数&#xff0c;每个整数占一行。输出格式 输出新数组中的所有元素&#xff0c;每个元素占一行。输出格式为 X[i] x&#xff0c;其中…

成都工业学院Web技术基础(WEB)实验七:Date、Math、Array对象使用

写在前面 1、基于2022级计算机大类实验指导书 2、代码仅提供参考&#xff0c;前端变化比较大&#xff0c;按照要求&#xff0c;只能做到像&#xff0c;不能做到一模一样 3、图片和文字仅为示例&#xff0c;需要自行替换 4、如果代码不满足你的要求&#xff0c;请寻求其他的…

pytest + yaml 框架 -59.用例失败重跑机制pytest-rerunfailures

前言 有些接口可能不太稳定&#xff0c;第一次跑的时候由于网络原因或者其它原因失败&#xff0c;但是重新跑2次又成功了。 对于这种需要重新跑几次的场景&#xff0c;可以使用用例失败重跑机制&#xff0c;需安装pytest-rerunfailures 插件。 场景示例 失败重跑需要依赖 py…