[多线程]一篇文章带你看懂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 就会自动把锁粗化, 避免频繁申请释
放锁.
 

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

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

相关文章

2024年创建有效SaaS PRD的终极指南

您目前是否正在开发SaaS产品?您是否想要确保您的产品满足目标用户的要求并符合您的业务目标?如果是这样,创建全面的“SaaS产品需求文档(PRD)”至关重要。 在了解SaaS PRD的具体组成部分之前,必须认识到内容…

前端必备-http知识

在掘金查看该文章 计算机网络五层模型 1.物理层 (Physical Layer) 关键词 光纤,电缆,双绞线,连接 物理层要解决的主要问题: (1)物理层要尽可能地屏蔽掉物理设备和传输媒体,通信手段的不同,使数据链路层感觉不到这些…

IT 人员与加密程序:如何战胜病毒

🔐 加密程序是攻击者在成功攻击组织时使用最多的恶意软件类型。它们通常会发送到一个庞大的电子邮件地址数据库,看起来像 Word 或 Excel 文档或 PDF 文件。 想象一下,你是会计部门的一名员工。这种格式的文件在电子文档管理系统中被广泛使用…

Linux服务器开发太麻烦? 试试IntelliJ IDEA公网远程访问开发极大提升开发效率

文章目录 1. 检查Linux SSH服务2. 本地连接测试3. Linux 安装Cpolar4. 创建远程连接公网地址5. 公网远程连接测试6. 固定连接公网地址7. 固定地址连接测试 本文主要介绍如何在IDEA中设置远程连接服务器开发环境,并结合Cpolar内网穿透工具实现无公网远程连接&#xf…

PySpark大数据处理详细教程

欢迎各位数据爱好者!今天,我很高兴与您分享我的最新博客,专注于探索 PySpark DataFrame 的强大功能。无论您是刚入门的数据分析师,还是寻求深入了解大数据技术的专业人士,这里都有丰富的知识和实用的技巧等着您。让我们…

IDEA快捷键注释代码设置不从行开头开始

我们平时在用IDEA开发项目时会发现,快捷键注释的//总是在代码的行开头上面,如下图所示: 这样就显得代码很不美观,那如何才能使注释//贴紧代码呢?需要在IDEA中进行如下配置: 点击Apply之后就可以了&#xff…

Win11 TensorRT环境部署

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

numpy.memmap 用法与注意事项

当处理大数组时,内存可能不够用。numpy 提供了一个函数 np.memmap() 让我们可以处理大数组。memmap memory mapped np.memmap() 可以读取大磁盘文件中的一小段到内存,所以它占内存较小 参数说明: import numpy as np from tempfile impo…

Gateway和spring-boot-starter-web的恩怨情仇

为什么取这个题目,其实与我踩到的坑有关,说起来这个坑非常神奇,这里面就涉及到Gateway和spring-boot-starter-web底层所依赖的技术不兼容的问题。 一、背景 SpringCloud 版本 ---- Finchley.SR2 SpringBoot 版本 ---- 2.0.6.RELEASE 如果同…

Bootstrap在弹框Povoper中显示图片

项目开发需要实现这个效果&#xff0c;当鼠标划过这个按钮的时候&#xff0c;会显示出指定的图片出来 HTML代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"…

高德地图+Vue中使用出现的问题

最近在做高德地图的逆向地理编码API出现了问题 按着官方的方式写代码运行时出现了问题&#xff0c;随后问了技术人员。 添加之后成功运行

“分割“安卓用户,对标iOS;鸿蒙将携手程序员的春天

近期关于“华为于明年推出不兼容安卓的鸿蒙版本”的消息传出&#xff0c;引起了业界的热议关注。自从2019年8月&#xff0c;美国制裁下&#xff0c;华为不再能够获得谷歌安卓操作系统相关付费服务&#xff0c;如此情况下&#xff0c;华为“备胎”鸿蒙操作系统一夜转正。华为鸿蒙…

IDEA新建jdk8 spring boot项目

今天新建spring boot项目发现JDK版本最低可选17。 但是目前用的最多的还是JDK8啊。 解决办法 Server URL中设置&#xff1a; https://start.aliyun.com/设置完成后&#xff0c;又可以愉快的用jdk8创建项目了。 参考 https://blog.csdn.net/imbzz/article/details/13469117…

新能源汽车生产污废水需要哪些工艺及设备

新能源汽车的快速发展带来了许多环境问题&#xff0c;其中之一就是生产过程中产生的污废水。由于新能源汽车的生产过程与传统汽车有所不同&#xff0c;因此需要采用特定的工艺和设备来处理和处理这些废水。 首先&#xff0c;新能源汽车生产过程中产生的污废水主要来自洗涤和冷却…

Certbot实现 HTTPS 免费证书(Let‘s Encrypt)自动续期

Certbot实现 HTTPS 自动续期 以前阿里云支持申请一年的免费https证书&#xff0c;那每年我们手动更新证书并没什么大问题&#xff0c;但现在阿里云的免费证书仅支持3个月&#xff0c;这意味着每三个月都要要申请一下证书显得非常麻烦。 下面我们使用Certbot实现ssl证书的自动…

后端打印不了trace等级的日志?-SpringBoot日志打印-Slf4j

在调用log变量的方法来输出日志时&#xff0c;有以上5个级别对应的方法&#xff0c;从不太重要&#xff0c;到非常重要 调用不同的方法&#xff0c;就会输出不同级别的日志。 trace&#xff1a;跟踪信息debug&#xff1a;调试信息info&#xff1a;一般信息warn&#xff1a;警告…

Java基础语法面试题

注释 什么Java注释 定义&#xff1a;用于解释说明程序的文字 分类 单行注释 格式&#xff1a; // 注释文字 多行注释 格式&#xff1a; /* 注释文字 /文档注释 格式&#xff1a;/* 注释文字 */ 作用 在程序中&#xff0c;尤其是复杂的程序中&#xff0c;适当地加入注释可…

结构体概念及应用

1.结构体类型的概念 在C语言中提供了很多基本的数据类型&#xff0c;但在实际开发中&#xff0c;无法满足程序中各种复杂数据的要求。有时需要将不同类型的数据组合成一个有机的整体&#xff0c;一边引用。例如&#xff1a; numnamesexagescore001lemonF18 90 在图中列举了…

VRRP协议详解

目录 一、基础概念 1、概念 2、VRRP的基本结构 状态机 二、VRRP主备备份工作过程 1、备份工作过程 2、VRRP的负载分担工作 三、实验 一、基础概念 1、概念 VRRP能够在不改变组网的情况下&#xff0c;将多台路由器虚拟成一个虚拟路由器&#xff0c;通过配置虚拟路由器的I…

【STM32入门】4.1中断基本知识

1.中断概览 在开展红外传感器遮挡计次的实验之前&#xff0c;有必要系统性的了解“中断”的基本知识. 中断是指&#xff1a;在主程序运行过程中&#xff0c;出现了特定的中断触发条件&#xff08;中断源&#xff09;&#xff0c;使得CPU暂停当前正在运行的程序&#xff0c;转…