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

news2024/10/5 12:32:18


目录

一.线程安全问题

二.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 {
        @Override
        public 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 {
        @Override
        public 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.coloradmin.cn/o/1669526.html

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

相关文章

【数轮】数论、质数、最大公约数、菲蜀定理

数学 唯一分解定理 n>2都可以表示为质因数的乘方。 令 n a1b1a2b2 … \dots … a1,b1 … \dots …都是质因数&#xff0c;b1,b2 … \dots …是对应质因数的数量。 调和级数 11/2 1/3 1/4 ⋯ \cdots ⋯ 1/ n 约等于 logn。 证明过程&#xff1a; 1/3 1/4 < (1/2) …

程序员最趁手的SVM算法,学完你会哭着感谢努力的自己!下篇.

支持向量机上篇内容更重要。 上篇地址&#xff1a;程序员最趁手的SVM算法&#xff0c;学完你会哭着感谢努力的自己&#xff01;上篇。-CSDN博客 废话不说直接进入主题&#xff1a; 6核贝叶斯支持向量机 核贝叶斯支持向量机通过学习一些已知的例子&#xff0c;并找到一个特殊…

【0DAY】湖南建研工程质量检测系统Download接口处存在任意文件读取漏洞

免责声明&#xff1a;文章来源互联网收集整理&#xff0c;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;所产生的一切不良后果与文章作者无关。该…

赋能数据库智能托管,Akamai 发布首款云计算业务线产品!

为了尽可能地简化数据库管理的复杂性&#xff0c;降低数据库成本&#xff0c;Akamai 在近期推出了首款 DBaaS&#xff08;数据库即服务&#xff09;产品——Linode Managed Database。这一数据库产品是 Akamai 自3月份收购 Linode 后发布的首款计算业务线产品。 一、更易用的数…

ipa 分区算法分析,图解

参考 Room Segmentation: Survey, Implementation, and Analysis. 分区算法调查&#xff0c;实现以及评估对比 相关论文 分区算法 New Brooms Sweep Clean - An Autonomous Robotic Cleaning Assistant for Professional Office Cleaning 形态分割 Interactive SLAM using …

EPICS database练习

给定一个以下的数据库&#xff1a; # 指定Limit的上限&#xff0c;初始为10&#xff0c;可以通过通道访问进行设置&#xff0c;上限为100 record(ao, "$(P)Limit") { field(DRVH, "100") field(DOL, "10") field(PINI, "YES") }# 一个…

vue3中的computed

一.computed用法 computed 计算属性就是当依赖的属性的值发生变化的时候&#xff0c;才会触发他的更改&#xff1b;如果依赖的值&#xff0c;不发生变化的时候&#xff0c;使用的是缓存中的属性值。 computed 属性是 Vue3 中的一个响应式计算属性&#xff0c;它可以根据其他响应…

谷歌Flank潜藏3年的Github Action供应链攻击

01 简 介 Flank [1] 是谷歌 Firebase Test lab 开源在 Github 的一个项目&#xff0c;用于同时对多个安卓和IOS设备进行测试。2024年4月15号 AWS 安全工程师 Adnan Khan 公布了关于该项目代码仓库 Github Action CI/CD 存在漏洞的细节[2]&#xff0c;漏洞在2020年于此 代码合…

LabVIEW二维码生成与识别

LabVIEW二维码生成与识别 随着数字化时代的快速发展&#xff0c;QR二维码作为一种高效的信息传递和识别手段&#xff0c;已广泛应用于各行各业。利用LabVIEW软件及其NI视觉开发模块(VDM)来实现一个高效的QR二维码生成与识别系统。该系统不仅能够快速生成带有自定义信息的二维码…

未授权访问:VNC未授权访问

目录 1、漏洞原理 2、环境搭建 3、未授权访问 防御手段 今天继续学习各种未授权访问的知识和相关的实操实验&#xff0c;一共有好多篇&#xff0c;内容主要是参考先知社区的一位大佬的关于未授权访问的好文章&#xff0c;还有其他大佬总结好的文章&#xff1a; 这里附上大…

C++的数据结构(五):树和存储结构及示例

在计算机科学中&#xff0c;树是一种抽象数据类型&#xff08;ADT&#xff09;或是实现这种抽象数据类型的数据结构&#xff0c;用来模拟具有树状结构性质的数据集合。这种数据结构以一系列连接的节点来形成树形结构。在C中&#xff0c;树的概念和存储结构是实现各种复杂算法和…

如何利用AI提高内容生产效率与AIGC典型案例分析

❤️❤️❤️ 欢迎来到我的博客。希望您能在这里找到既有价值又有趣的内容&#xff0c;和我一起探索、学习和成长。欢迎评论区畅所欲言、享受知识的乐趣&#xff01; 推荐&#xff1a;数据分析螺丝钉的首页 格物致知 终身学习 期待您的关注 导航&#xff1a; LeetCode解锁100…

DI-engine强化学习入门(八)如何高效构建决策环境

本文章将介绍 DI-engine 中用于高效构建和标准化不同类型决策环境的系列工具&#xff0c;从而方便我们将各式各样的原始决策问题转化为适合使用强化学习方法解决的形式。 前言 对于“决策环境复杂性”的问题&#xff0c;这里描述的是在强化学习&#xff08;RL&#xff09;领域…

torch 单机 多卡 训练(二)

pytorch.distributed.launch和torchrun的对比 多卡训练 真的烦 并行训练最大的好处&#xff0c;在于GPU内存变大&#xff0c;不是变快 torch.distributed.launch CUDA_VISIBLE_DEVICES0,1,2,3 python -m torch.distributed.launch --nproc_per_node4 --nnodes1 --use_env k…

获取Linux上的Redis的用户名、密码、端口、host等信息

目录 进入redis-cli的目录 启动./redis-cli服务 查询密码 查询用户名 查询端口 查询host 参考文章&#xff1a;解决redis-cli连接时出现Could not connect to Redis at 127.0.0.1:6379: Connection refused-阿里云开发者社区 (aliyun.com) linux查看redis用户和密码_mo…

最新版★重大升级★神点云连锁餐饮V2独立版点餐系统★公众号/h5/小程序前后端全套源码

提醒&#xff1a; 市场上流通很多老版本代码&#xff0c;一大堆问题且无法保证售后的源码&#xff0c;请各位买家一定要睁大眼睛&#xff0c;以防上当受骗&#xff01;&#xff01;&#xff01;本系统源码全是经本人亲自测试与修复的完好版本&#xff0c;且本人用此版本源码已…

面试官:说说你对Node.js 的理解?优缺点?应用场景?

一、是什么 Node.js 是一个开源与跨平台的 JavaScript 运行时环境 在浏览器外运行 V8 JavaScript 引擎&#xff08;Google Chrome 的内核&#xff09;&#xff0c;利用事件驱动、非阻塞和异步输入输出模型等技术提高性能 可以理解为 Node.js 就是一个服务器端的、非阻塞式I/…

EDA设计学习笔记2:STM32F103C8T6最小系统板的仿绘

今日开始仿制练习一个STM32F103C8T6最小系统板&#xff0c;通过对这个最小系统板的仿制&#xff0c;达到对自己PCB设计的练习的目的&#xff0c;最终目标是自己设计出一块PCB&#xff0c;做一个OLED的桌面小摆件...... 也不知道画出来能不能用..... 目录 主控芯片的搜索与放置…

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

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

Centos 7.9如何使用源码编译安装curl最新版本

文章目录 1、前言2、curl源代码下载3、openssl安装4、编译curl4.1、配置编译环境4.2、编译输出二进制curl程序4.3、安装编译后的curl4.4、编译完成检查4.5、验证安装 1、前言 Centos 7.9&#xff0c;由于系统为2017年发行&#xff0c;且以稳定性为主&#xff0c;部分工具版本较…