图文详解:synchronized关键字 及其底层原理


目录

一.线程安全问题

二.synchronized关键字

▐ synchronized图解

▐ 可重入锁及图解

▐ synchronized用于方法上

三.Java标准库中synchronized的使用

四.synchronized的底层实现原理


一.线程安全问题

线程安全是指在多线程环境下,对共享资源的访问不会导致数据不一致或出现意外结果的特性。在多线程程序中,多个线程可以同时访问和操作共享数据,如果没有适当的同步机制和保护措施,可能会导致数据竞争和不一致的问题

线程安全的实现可以通过使用互斥锁、信号量、原子操作等方法来保证。互斥锁可以保证同一时刻只有一个线程可以访问共享资源,其他线程需要等待锁的释放。信号量可以控制同时访问共享资源的线程数量。原子操作是指不可分割的操作,在执行过程中不会被其他线程中断,可以保证数据的一致性。

下面是一个简单的示例代码,展示了线程不安全的情况:

public class UnsafeThreadDemo {private static int counter = 0;public static void main(String[] args) {Thread thread1 = new Thread(new IncrementCounter());Thread thread2 = new Thread(new IncrementCounter());thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Counter: " + counter);}static class IncrementCounter implements Runnable {@Overridepublic void run() {for (int i = 0; i < 100000; i++) {counter++;}}}
}

在这个示例中,我们创建了两个线程,并且它们都执行相同的任务:对 counter 变量进行递增操作。每个线程将 counter 递增 100000 次。我们期望最终的结果是 counter 的值为 200000。然而,由于线程不安全,最终的结果很可能不是我们期望的值。

运行结果:

这是因为线程之间可以并发地访问和修改 counter 变量,而没有任何同步机制来保护它。如果两个线程同时读取并且递增 counter 的值,那么它们可能会读取到相同的值并递增相同的值,导致最终结果比期望的小一些。

要解决上述的问题,我们可以使用同步机制,例如使用 synchronized 关键字或 Lock 接口来保护共享变量的访问。这样可以确保在任何时候只有一个线程能够访问和修改 counter 的值,避免了线程不安全的情况。

二.synchronized关键字

在Java中,synchronized 是一个关键字,用于实现线程同步。当一个方法或一个代码块被synchronized修饰时,它被称为同步方法或同步代码块。这意味着每次只有一个线程可以进入该方法或代码块,其他线程必须等待,直到当前线程执行完毕并释放锁。

synchronized关键字的作用是防止多个线程同时执行同步方法或代码块,从而避免竞态条件(race condition)和数据不一致性问题。它确保了多个线程之间的协调和同步,使得共享资源可以被安全地访问和修改。

竞态条件(Race Condition)是指在多线程环境下,由于线程执行顺序的不确定性,导致程序的执行结果不确定或出现错误的情况。简单来说,就是多个线程对共享资源的访问顺序不确定,可能会导致不符合预期的结果。

 synchronized 的语法如下:

 synchronized(对象) {//用于保护的代码
}

在使用synchronized时,需要传入一个对象作为锁。这个对象的具体含义是锁定的对象,也就是说,只有持有该对象的锁的线程才能执行被synchronized修饰的代码块或方法,其他线程必须等待直到锁被释放。这个对象可以是任意对象,但通常情况下,为了确保正确性和可读性,我们会选择一个特定的对象作为锁。

传入不同的对象就相当于使用了不同的锁。每个对象都有一个相关联的监视器(monitor),也可以说是一个锁。当一个线程进入synchronized代码块时,它必须先获得与传入对象相关联的监视器,才能执行代码块中的内容。因此,如果你传入不同的对象作为锁,那么这些对象就会对应不同的监视器,也就是说,它们是不同的锁。

这个特性很有用,因为它允许程序员精细地控制哪些代码块需要同步,哪些不需要。比如可以为不同的代码块传入不同的锁对象,以避免它们之间的互相阻塞。

对于刚才的代码,我们使用 synchronized 就可以进行改进,对于每一次 counter 变量递增的时候我们都使用synchronized 对齐进行上锁,保护其中的临界区代码

public class SafeThreadDemo {private static int counter = 0;public static void main(String[] args) {Thread thread1 = new Thread(new IncrementCounter());Thread thread2 = new Thread(new IncrementCounter());thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Counter: " + counter);}static class IncrementCounter implements Runnable {@Overridepublic void run() {for (int i = 0; i < 100000; i++) {synchronized (SafeThreadDemo.class) {counter++;}}}}
}

在这个优化后的代码中,我们使用了SafeThreadDemo.class作为锁对象,以确保只有一个线程能够同时访问counter变量,从而避免了竞态条件,使得代码线程安全。 

▐ synchronized图解

看完了以上的说明,相信你对synchronized关键字已经有了较为深刻的理解,用图示可以表示如下

如图,小人就相对于是一个个线程,每个房间则对应了synchronized关键字的锁对象,不同的锁对象就对应了不同的房间。当线程小人请求进入房间的时候就会进行判断,判断是否能够获取当前的锁对象,如果能获取则让该线程小人进入房间完成该线程对应的工作,并且对这个房间上锁,当其他线程小人来了后就会访问这个房间的锁,如果房间被锁上了,那么该线程小人就会阻塞等待。当房间内部的线程小人完成了他的工作后就会解开房间的锁,从而也就保证了线程的安全性。而不同的房间对应的房间钥匙也就是锁自然也是不一样,这就保证了我们对于资源的灵活分配。

▐ 可重入锁及图解

另外,synchronized实现的锁属于是可重入锁,还是用这个图示来说明:

当线程1因为时间片的分配等问题临时离开房间,失去了房间的使用权后,线程1为了确保工作的顺利完成,就并没有释放掉锁,当线程1后续被操作系统重新调度进入房间2后,他就可以继续完成之前的手头工作,对于这样的允许一个线程重复进入访问锁直到锁被释放的情况,我们就称之为该锁为可重入锁。

可重入锁(Reentrant Lock)也称为递归锁,是一种线程同步机制。可重入锁允许重复获取同一把锁,使得线程可以在持有锁的情况下再次获取该锁,而不会造成死锁。这种机制使得可重入锁可以用于同步嵌套的代码块。

可重入锁的内部实现通常会维护一个线程持有锁的计数器,并记录当前持有锁的线程。当一个线程首次尝试获取锁时,计数器会增加,并记录该线程。如果同一个线程再次尝试获取锁,计数器会递增,而不是阻塞。只有当计数器归零时,锁才会释放,允许其他线程获取锁。

▐ synchronized用于方法上

synchronized也可以直接作用于成员方法之上,相对于锁住的就是this对象,例如

class Test {public synchronized void test() {}
}
//等价于
class Test {public void test() {synchronized (this) {}}
}

它也可以作用于静态方法上,它锁住的相对于就是类对象

class Test {public synchronized static void test() {}
}
//等价于
class Test {public static void test() {synchronized (Test.class) {}}
}

三.Java标准库中synchronized的使用

Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施,比如:

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder

但是还有一些是线程安全的. 使用了一些锁机制来控制. 

  • Vector
  • HashTable
  • ConcurrentHashMap
  • StringBuffer

还有的虽然没有加锁, 但是不涉及 "修改", 仍然是线程安全的

  • String

比如在StringBuffer的核心方法中,基本上都加的有 synchronized 

四.synchronized的底层实现原理

synchronized也可在底层的实现主要依赖于锁监视器monitor,在Java中,monitor是一种同步机制,用于保护共享资源的线程安全性。

Java中的monitor是通过内置锁(也称为监视器锁)来实现的。每个Java对象都可以关联一个Monitor对象,我们称之为内置锁,当一个线程进入synchronized方法或块时,它会自动获取该对象的内置锁,并在执行完synchronized代码段后释放锁。这种机制确保了同一时刻只有一个线程可以访问被synchronized保护的代码。只有在持有monitor锁的线程释放锁后,其他线程才能获取锁并执行对共享资源的访问。

这样的说明未免有点枯燥不好理解,笔者这里还是给出图示:

对于每一个Java对象都可以绑定一个Monitor对象(锁),当多个线程来执行被synchronized修饰的同步代码块时,根据JDK的调度机制会选取其中一个线程来作为该对象绑定的Monitor对象的拥有者(Owner),并且一个Monitor对象只能有一个锁主人(Owner),然后该线程便获得了执行该同步代码块的权利,而对于那些没有被选中的线程则会放入一个等待队列(EntryList)中进行等待,只有当前线程完成工作后才会更新调度规则选出新的Owner。

需要注意的是,Java中的monitor是一种高级抽象,实际上是由底层的操作系统提供的同步原语来实现的。

▐ 基于锁策略的synchronized原理

以上关于synchronized的讲解是属于在代码层次上的原理,关于锁还有一部分很重要的就是锁的策略,尤其对于synchronized来说,她有以下的一些特性:

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

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

在这里对这些名词简单的解释一下,更具体的信息则需要锁策略相关的知识来说明:

锁策略详解:互斥锁、读写锁、乐观锁与悲观锁、轻量级锁与重量级锁、自旋锁、偏向锁、可重入锁与不可重入锁、公平锁与非公平锁-CSDN博客

偏向锁

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

自旋锁

自旋锁是一种基于循环重试的锁机制,在多线程编程中用于实现对共享资源的互斥访问。当一个线程尝试获取自旋锁时,如果锁已被其他线程持有,该线程不会立即进入阻塞状态,而是在循环中不断尝试获取锁,直到成功获取为止,或者达到最大尝试次数后才会放弃。

轻量级和重量级锁

轻量级锁是为了在多线程竞争情况下,提高性能而引入的一种锁优化技术。当一个线程尝试获取锁时,如果锁没有被其他线程占用,虚拟机会在当前线程的栈帧中使用 CAS 操作尝试将对象头部的 Mark Word 替换为指向当前线程的锁记录指针(Lock Record Pointer)。如果 CAS 操作成功,当前线程就获得了锁,并且锁的状态被标记为轻量级锁。此时其他线程访问同步块时会尝试自旋等待,而不是直接阻塞,以减少线程切换的开销。如果自旋等待一段时间后仍无法获取锁,或者其他线程争用激烈,CAS 操作失败,那么轻量级锁会膨胀为重量级锁。当轻量级锁膨胀失败时,锁会升级为重量级锁。重量级锁会使其他线程阻塞,而不是进行自旋等待,防止CPU空转浪费资源。




 本次的分享就到此为止了,希望我的分享能给您带来帮助,创作不易也欢迎大家三连支持,你们的点赞就是博主更新最大的动力!如有不同意见,欢迎评论区积极讨论交流,让我们一起学习进步!有相关问题也可以私信博主,评论区和私信都会认真查看的,我们下次再见

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

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

相关文章

Python构建网络控制和管理应用程库之使用详解

概要 POX是一种广泛使用的Python开发工具,主要用于构建网络控制和管理应用程序。作为一个灵活的软件平台,POX支持快速开发网络通信协议,尤其是在软件定义网络(SDN)领域中得到了广泛应用。本文将全面介绍POX库的安装、主要特性、基本与高级功能,并结合实际应用场景,展示…

AVL树!

文章目录 1.AVL树的概念2.AVL树的插入和旋转3.AVL树的旋转3.1旋转的底层&#xff1a;3.2 右旋转3.3 左旋转3.4 双旋 4.AVL树的底层 1.AVL树的概念 当向二叉搜索树中插入新结点后&#xff0c;如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整)&a…

字符串取第一个空格之后的所有的值字符串取第一个空格之后的第二个元素的值,不要后面的值

当我们后端返回值可能存在某些特定的值或标识导致返回数据不固定且是空格分割时&#xff0c;我们想取出返回字符串中的某个值&#xff0c;就可以参考下面对这个字符串进行操作提取&#xff0c;当然&#xff0c;如果是别的符号分割开的把下面的空格替换即可 1、字符串取第一个空…

根据特定条件在列表中加一列操作,符合此条件时此列才会展示

我们想要列表中有一列数据在A环境打开是显示的&#xff0c;在B环境打开则不显示&#xff0c;这里B环境表示为默认环境 1、不能直接用环境判断加在列表的前面&#xff0c;否则其他环境会出现空格情况 constructor(props) {super(props)const columns [{ title: 姓名, dataInd…

在全志H616核桃派开发板上进行PyQt5的代码编写和运行

核桃派本地 在上一节我们通过Qt Designer设计了ui窗口并转换成了Python代码&#xff0c;由于是Python编程&#xff0c;因此我们可以在核桃派开发板打开Python代码进行编程。 在核桃派上推荐使用Thonny来打开编写Python文件, 使用请参考&#xff1a;Thonny IDE。 打开上一节生…

Java并发编程:Thread原理解析

文章目录 一、java中的thread和操作系统中的Thread对应关系 一、java中的thread和操作系统中的Thread对应关系 在java中用户线程和内核线程是1:1的形式&#xff1a; 其中java层面创建的线程为用户线程&#xff0c;其对应的底层线程为内核线程。 Java生成线程的流程如下&#…

【JavaEE】【1.3 Servlet】1.3.3 HttpServletRequest的应用

Http报文结构 请求报文 HTTP的请求报文由四部分组成&#xff08;请求行请求头部空行请求体&#xff09;&#xff1a; 请求行&#xff08;Request Line&#xff09;①②③&#xff1a; ① 请求方法&#xff08;Method&#xff09;&#xff1a;要执行的HTTP操作&#xff0c;…

el-menu 保持展开点击不收缩 默认选择第一个菜单

<el-menu:default-openeds"[/system]" 数组 默认展开第一个:collapse"isCollapse"close"handleClose" 点击关闭的时候 让菜单打开 就可以实现保持展开效果ref"menus":unique-opened"true":active-text-color"se…

JVM 类的加载器分类与测试

文章目录 1. 类加载器父类说明2. 子父类加载器关系3. 具体类的加载器介绍3.1 引导类加载器3.2 扩展类加载器3.3 系统类加载器 4. 用户自定义类加载器5. 测试不同的类加载器 1. 类加载器父类说明 JVM 支持两种类型的类加载器&#xff0c;分别为引导类加载器&#xff08;Bootstr…

docker(四):数据卷

数据卷 卷的设计目的就是数据的持久化&#xff0c;完全独立于容器的生存周期&#xff0c;因此Docker不会在容器删除时删除其挂载的数据卷。 1、docker run docker run -it --privilegedtrue -v /宿主机绝对路径目录:/容器内目录 镜像名2、挂载注意事项 --privilegedtru…

Spring Security 复盘

1、什么Spring Security&#xff1f; Spring Security 是一种强大的框架&#xff0c;它在 Spring 生态系统中扮演着保护应用安全的关键角色。Spring Security 基于 Spring 框架&#xff0c;提供了一套 Web 应用安全性的完整解决方案。 2、认证 和 授权 1.什么是认证&#xff1…

基于JAVA的微信小程序二手车交易平台(源码)

博主介绍&#xff1a;✌程序员徐师兄、8年大厂程序员经历。全网粉丝15w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

uniapp 安卓证书导出成cer文件 查看公钥

// your_alias 换成 证书详情中的别名&#xff0c;your_keystore.keystore 改成自己的证书文件名 keytool -export -alias your_alias -file certificate.cer -keystore your_keystore.keystore双击生成的cer文件 可以查看到证书的详细信息 其中就包括证书的公钥

安捷伦E8363B详情资料e8363b矢量网络分析仪E8363B 40G

E8363B E8363B E8363B 品牌&#xff1a;Agilent 型号&#xff1a;E8363B PNA系列网络分析仪 频率范围&#xff1a;10MHz~40GHz 动态范围&#xff1a;123dB 主要特性与技术指标 #110 dB的动态范围&#xff0c;<0.006 dB的迹线噪声 #<26微秒/点的测量速度&#xff0c;32个…

定期更新与维护:技术与生活的同步律动

在这个数字化时代&#xff0c;科技的温暖之光照进了盲人朋友们的日常生活中&#xff0c;特别是那些辅助出行的应用程序&#xff0c;它们如同贴心的向导&#xff0c;引领着用户穿越城市的喧嚣与宁静。然而&#xff0c;要确保这些应用始终能够高效、安全地服务于盲人用户&#xf…

令牌桶算法:如何优雅地处理突发流量?

令牌桶算法的介绍 在网络流量控制和请求限流中&#xff0c;令牌桶算法是一种常用的策略。那么&#xff0c;令牌桶算法到底是什么呢&#xff1f;它的工作原理又是怎样的呢&#xff1f;让我们一起来探索一下。 令牌桶算法&#xff0c;顾名思义&#xff0c;就是有一个存放令牌的…

【VTKExamples::Rendering】第六期 TestFlatVersusGround

很高兴在雪易的CSDN遇见你 VTK技术爱好者 QQ:870202403 公众号:VTK忠粉 前言 本文分享VTK样例TestFlatVersusGround,希望对各位小伙伴有所帮助! 感悟:自身优秀很重要,让别人觉得你很优秀更重要! 感谢各位小伙伴的点赞+关注,小易会继续努力分享,一起进步! …

云计算第十二课

安装虚拟机 第一步新建虚拟机 选择自定义安装 下一步 选择稍后安装操作系统 选择系统类型和版本 选择虚拟机文件路径&#xff08;建议每台虚拟机单独存放并且路径不要有中文&#xff09;点击下一步 选择bios下一步 选择虚拟机处理器内核数量 默认硬盘或者自行调大硬盘 选择虚…

【Java】:向上转型、向下转型和ClassCastException异常

目录 先用一个生动形象的例子来解释向上转型和向下转型 向上转型&#xff08;Upcasting&#xff09; 向下转型&#xff08;Downcasting&#xff09; 向上转型 概念 例子 发生向上转型的情况 1.子类对象赋值给父类引用 2.方法参数传递 3.返回值 向下转型 概念 注意…

Electron学习笔记(一)

文章目录 相关笔记笔记说明 一、轻松入门 1、搭建开发环境2、创建窗口界面3、调试主进程 二、主进程和渲染进程1、进程互访2、渲染进程访问主进程类型3、渲染进程访问主进程自定义内容4、渲染进程向主进程发送消息5、主进程向渲染进程发送消息6、多个窗口的渲染进程接收主进程发…