图文详解: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 {
        @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.mfbz.cn/a/619784.html

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

相关文章

详解循环队列——链表与数组双版本

前言&#xff1a;本节内容主要是讲解循环队列。 在本篇中会讲到两个版本——数组版本、链表版本。本篇内容适合正在学习数据结构队列章节或者已经学过队列但对循环队列感觉模糊的友友们 。 首先先来看一下什么是循环队列 什么是循环队列 因为是刚开始讲解&#xff0c; 所以我们…

【基础绘图】 10.饼图

效果图&#xff1a; 主要步骤&#xff1a; 1. 数据准备&#xff1a;自己赋值的随机数 2. 图像绘制&#xff1a;绘制饼图 详细代码&#xff1a;着急的直接拖到最后有完整代码 步骤一&#xff1a;导入库包及图片存储路径并设置中文字体为宋体&#xff0c;西文为新罗马&#…

totoriseSVN 常见问题

1. SVN 无法 clean up 上传时没有关闭 Excel&#xff0c;导致传入了一些临时文件&#xff08;文件名以$开头&#xff09;&#xff0c;关闭文件后临时文件自动删除&#xff0c;导致 SVN 版本错乱&#xff0c;使用 CleanUp 功能无效 更新时提示【Previous operation has not fin…

win7 phpstudy 多站点无法保存hosts的原因

1、先找到hosts文件位置 C:\Windows\System32\drivers\etc hosts文件不是txt的后缀&#xff0c;它是一个系统文件 2、如果不显示需要查找隐藏文件 组织-》文件夹和搜索选项-》查看-》取消隐藏文件夹的的√ 3、文件无法编辑 属性不要勾选只读

【SAP-FICO】SAP-FICO生产订单-结算规则配置路径(OKO7)

需求&#xff1a; 作为一个ABAPer&#xff0c;有接到一个狗屁倒灶的配置需求&#xff0c;要求如下&#xff0c;给生产订单的结算规则显示出来 图1&#xff1a;找一个生产订单&#xff0c;显示其结算规则 CO03→菜单栏-表头→结算规则 图2&#xff1a;查看该生产订单&#xff0c…

SMB/RPC协议分析之-命名/匿名管道pipe

在前面的文章中&#xff0c;介绍了SMB协议共享相关的内容&#xff0c;详见我的专栏《网络攻防协议实战分析》&#xff0c;连接这里。在SMB协议中往往需要连接到对应的远程管道&#xff0c;如果你经常接触到SMB协议&#xff0c;相信你对于lsass&#xff0c;svcctl等多种命名管道…

数据结构-二叉树-AVL树(平衡二叉树)

红黑树是平衡二叉树的一个变种。 一、 产生平衡二叉树的原因。 二叉搜索树的问题在于极端场景下退化为类似链表的结构&#xff0c;所以搜索的时间复杂度就变成了O(N)。为了保证二叉树不退化为链表&#xff0c;我们必须保证二叉树的的平衡性。 二叉平衡搜索树就是解决上面的问…

职场新人小王的沟通挑战与成长

近日&#xff0c;职场新人小王遇到了一个沟通上的小难题。作为刚刚踏入社会的新鲜人&#xff0c;小王在工作会议上因为一次直接的反馈而无意间触动了同事的敏感神经&#xff0c;导致双方关系稍显紧张。 在一次团队会议上&#xff0c;小王被要求分享对项目进度的看法以及建议。他…

【图解计算机网络】TCP 重传、滑动窗口、流量控制、拥塞控制

TCP 重传、滑动窗口、流量控制、拥塞控制 TCP 重传超时重传快速重传 滑动窗口流量控制拥塞控制慢启动拥塞避免拥塞发生快速恢复 TCP 重传 TCP重传是当发送的报文发生丢失的时候&#xff0c;重新发送丢失报文的一种机制&#xff0c;它是保证TCP协议可靠性的一种机制。 TCP重传…

9. SVG中的text元素

SVG (Scalable Vector Graphics) 提供了强大的文本渲染能力&#xff0c;其中<text>元素是常用 的文本操作的元素。本文将详细介绍<text>标签的基本使用方法&#xff0c;并展示如何通过<tspan>和<textPath>增强文本的表现力。 <text>标签基础 &…

【PHP【实战项目】系统性教学】——使用最精简的代码完成用户的登录与退出

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;开发者-曼亿点 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 曼亿点 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a…

MyBatis——MyBatis 参数处理

一、单个简单类型参数 简单类型包括&#xff1a; byte short int long float double char Byte Short Integer Long Float Double Character String java.util.Date java.sql.Date parameterType 属性&#xff1a;告诉 MyBatis 参数的类型 MyBatis 自带类型自动推断机制…

【Linux】centos7安装软件(rpm、yum、编译安装),补充:查找命令的相关文件路径,yum安装mysql

【Linux】技术上&#xff0c;Linux是内核。而术语上&#xff0c;我们通常说的Linux是完整的操作系统&#xff0c;其实称为"Linux发行版"&#xff0c;是将Linux内核和应用系统打包&#xff0c;由不同的发行家族发行了不同版本。Linux发行版众多&#xff0c;主要有RedH…

Debian常用命令:高效管理与运维的必备指南

在Linux世界中&#xff0c;Debian以其稳定性、安全性和开源精神赢得了广大用户的青睐。作为一个基于Linux的操作系统&#xff0c;Debian拥有丰富且强大的命令行工具&#xff0c;这些命令对于系统管理员和开发者来说至关重要。本文将为您介绍一系列Debian系统中的常用命令&#…

基于Javaee的影视创作论坛的设计与实现+vue论文

系统简介 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装影视创作论坛软件来发挥其高效地信息处理的作用&#xf…

询问贴:这要怎么设置捏,寻思着总不该一个一个挖空吧????

这要怎么设置捏&#xff0c;寻思着总不该一个一个挖空吧&#xff1f;&#xff1f;&#xff1f;&#xff1f;

【javaSE】认识异常(1)

❤️❤️前言~&#x1f973;&#x1f389;&#x1f389;&#x1f389; hellohello~&#xff0c;大家好&#x1f495;&#x1f495;&#xff0c;这里是E绵绵呀✋✋ &#xff0c;如果觉得这篇文章还不错的话还请点赞❤️❤️收藏&#x1f49e; &#x1f49e; 关注&#x1f4a5;&a…

联丰策略股票杠杆股票交易市场突破3100点!A股稳了?

查查配近期,大盘再次来到3100点附近。 重要关口得到有效突破,市场情绪明显升温,甚至有投资者高喊:反转已经在路上!但也有谨慎者认为,市场仍有回调风险,围绕3000点震荡或是接下来的主旋律。 联丰策略拥有一支由知名互联网公司和国内证券金融机构的行业专家组成的一流运营团队。…

HTML炫酷的相册

目录 写在前面 HTML简介 完整代码 代码分析 系列推荐 写在最后 写在前面 本期小编给大家带来一个炫酷的旋转相册&#xff0c;快来解锁属于你的独家记忆吧&#xff01; HTML简介 HTML&#xff08;全称为超文本标记语言&#xff09;是一种用于创建网页结构和内容的标记语…

Python 编程语言中的 None 到底是什么?

&#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ 让我们一起深入了解 Python 中的 None。 什么是 None&#xff1f; 在 Python 编程语言中&#xff0c;None 是一个特殊的常量&#xff0c;它代表了 “无” 或 “没有值”。你可以把它想象成一个空盒子…