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

news2024/12/23 2:23:41

目录

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 就会自动把锁粗化, 避免频繁申请释
放锁.
 

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

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

相关文章

十五 动手学深度学习v2计算机视觉 ——全连接神经网络FCN

文章目录 FCN FCN 全卷积网络先使用卷积神经网络抽取图像特征,然后通过卷积层将通道数变换为类别个数,最后通过转置卷积层将特征图的高和宽变换为输入图像的尺寸。 因此,模型输出与输入图像的高和宽相同,且最终输出通道包含了该空…

【渗透测试】常用的8种火狐插件

1、Max HacKBar 推荐理由:免费的hackbar插件,可快速使用SQL注入、XSS和Bypass等payload进行测试,可进行多种编码和解码,安装后F12即可使用。 2、FoxyProxy Standard 推荐理由:FoxyProxy是一个高级的代理管理工具&am…

如何了解蜘蛛池蚂蚁SEO

蜘蛛池是一种基于搜索引擎优化的技术手段,通过模拟蜘蛛爬行行为来提高网站在搜索引擎中的排名,从而增加网站的流量和曝光率。 编辑搜图 如何联系蚂蚁seo? baidu搜索:如何联系蚂蚁SEO? baidu搜索:如何联…

基于JavaWeb+SSM+Vue微信小程序的移动学习平台系统的设计和实现

基于JavaWebSSMVue微信小程序的移动学习平台系统的设计和实现 源码获取入口Lun文目录前言主要技术系统设计功能截图订阅经典源码专栏Java项目精品实战案例《500套》 源码获取 源码获取入口 Lun文目录 第1章 绪论 1 1.1 课题背景 1 1.2 课题意义 1 1.3 研究内容 2 第2章 开发环…

ospf 知识总结

ospf 知识总结 一、ospf的概念 - 开放式最短路径优先协议,是广泛使用的一种动态路由协议,它属于链路状态路由协议,是一个内部网关协议(IGP),用于在单一自治系统(AS)内决策路由。 - …

cmake 从零开始源码安装(Ubuntu系统)

Ubuntu 系统安装 1、安装编译工具和依赖库 ## 必要的 sudo apt install gsudo apt install make## 与make 同等级的构建工具,为了演示而安装的 sudo apt install ninja-build## 压缩工具库 sudo apt install unzip## 加密和传输(根据系统名称可能不一样…

如何使用蜘蛛池蚂蚁SEO

​蜘蛛池是一种利用搜索引擎爬虫进行推广营销的方式。它的核心是建立一个能够吸引搜索引擎爬虫的网站群,这些网站能够产生大量的优质内容,并形成一个巨大的网站群,从而吸引更多的搜索引擎爬虫。 如何联系蚂蚁seo? baidu搜索&…

JVM类加载器的分类以及双亲委派机制

目录 前言 1. 类加载器的分类: 1.1 启动类加载器(Bootstrap ClassLoader): 1.2 扩展类加载器(Extension ClassLoader): 1.3 应用程序类加载器(Application ClassLoader&#xff…

一款提高嵌入式代码质量的工具

我们通常认为,在中断中,不能执行耗时的操作,否则会影响系统的稳定性,尤其对于嵌入式编程。 ​ 对于带操作系统的程序而言,可以通过操作系统的调度,将中断处理分成两个部分,耗时的操作可以放到线…

当心这30个重要漏洞!微软发布12月补丁日安全通告

近日,亚信安全CERT监测到微软12月补丁日发布了34个漏洞的安全补丁(不包含此前发布的Microsoft Edge等安全更新),其中,4个被评为紧急,30个被评为重要。包含10个权限提升漏洞,8个远程代码执行漏洞…

cat EOF快速创建一个文件,并写入内容

在linux系统中,如果你有这个需求 vi一个文件 /etc/docker/daemon.json 在这个文件中写入内容 { "registry-mirrors": ["https://iw3lcsa3.mirror.aliyuncs.com","http://10.1.8.151:8082"],"insecure-registries":[&quo…

fastadmin表格无刷新行内编辑(列表点击字段编辑)

功能介绍 此插件是一款基于x-editable实现的表格无刷新行内编辑功能的插件。 使用方法 首先我们需要在我们当前的控制器所对应的JS文件头部添加依赖,追加一个editable,如下: define([jquery, bootstrap, backend, table, form,

深入理解 hash 和 history:网页导航的基础(上)

🤍 前端开发工程师(主业)、技术博主(副业)、已过CET6 🍨 阿珊和她的猫_CSDN个人主页 🕠 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 🍚 蓝桥云课签约作者、已在蓝桥云…

【基础篇】1.1 认识STM32(二)

3.3 VREF/VREF-引脚 VREF和VREF-是STM32中用于提供参考电压的引脚。如下图: VREF引脚可以连接一个单独的外部参考电压,范围在2.0V~VDDA,但不能超过VDDA,否则就超过了模拟器件的最大供电电压。在100引脚的封装中&#…

西工大网络空间安全学院计算机网络实验五——ACL配置

实验五、ACL配置 一. 实验目的 1. 掌握ACL的基本配置方法 二. 实验内容 1. 基于如下图所示的拓扑图,对路由器进行正确的RIP协议配置; ​ 首先引入3台2811 IOS15型号的路由器、3台2950-T24型号的交换机、4台PC-PT型号的PC机、两台Server-PT型号的服务…

深圳锐科达IP网络广播系统

深圳锐科达IP网络广播系统 网络音频广播系统是一种基于TCP/IP网络的纯数字音频广播系统。该网络音频广播系统在物理结构上与标准IP网络完全集成。它不仅真正实现了基于TCP/IP网络的数字音频的广播、直播和点播,而且利用TCP/IP网络的优势,突破了传统模拟广…

Goldstein枝切法对存在间断相位缺陷的解缠研究

摘要: Goldstein枝切法作为相位解缠中路径积分法的重要算法之一,其解缠结果易受到噪声或间断相位缺陷所引起的残差点影响。为了研究相位间断缺陷对解缠算法的影响,模拟了具有间断相位缺陷的数据,采用 Gold-stein枝切法进行了系统的解缠研究。…

二百一十五、Flume——Flume拓扑结构之复制和多路复用的开发案例(亲测,附截图)

一、目的 对于Flume的复制和多路复用拓扑结构,进行一个小的开发测试 二、复制和多路复用拓扑结构 (一)结构含义 Flume 支持将事件流向一个或者多个目的地。 (二)结构特征 这种模式可以将相同数据复制到多个channe…

C语言的system函数简介

函数原型 包含在头文件 “stdlib.h” 中 int system(const char * command) 函数功能 执行 dos(windows系统) 或 shell(Linux/Unix系统) 命令,参数字符串command为命令名。另,在windows系统下参数字符串不区分大小写。 说明:在windows系统中&…

机器翻译:跨越语言边界的智能大使

导言 机器翻译作为人工智能领域的瑰宝,正在以前所未有的速度和精度,为全球沟通拓展新的可能性。本文将深入研究机器翻译的技术原理、应用场景以及对语言交流未来的影响。 1. 简介 机器翻译是一项致力于通过计算机自动将一种语言的文本翻译成另一种语言的…