java并发编程:synchronized关键字与锁详解

news2024/11/27 2:28:22

文章目录

  • 线程安全问题
  • synchroinzed关键字
  • 几种锁
    • Java对象头
    • 偏向锁
    • 轻量级锁
    • 自旋锁
    • 重量级锁
    • 锁升级的场景
  • JVM 是如何实现 synchronized 的?
  • 小结


这篇文章我们来聊一聊Java多线程里面的“锁”。

首先需要明确的一点是:Java多线程的锁都是基于对象的,Java中的每一个对象都可以作为一个锁。

还有一点需要注意的是,我们常听到的类锁其实也是对象锁。

Java类只有一个Class对象(可以有多个实例对象,多个实例共享这个Class对象),而Class对象也是特殊的Java对象。所以我们常说的类锁,其实就是Class对象的锁。

我们先来看下多线程下为什么会存在线程安全问题?

线程安全问题

一个变量 a, A 或者线程 B 单独访问并且修改变量 i 的值没有任何问题,那如果并行的修改变量 i ,那就会有安全性问题。

public class Test {

    public static int a = 0;

    public static void addA(){
        a++;
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
       		new Thread(new Runnable() {
                @Override
                public void run() {
                    addA();
                }
            }).start();
        }
        Thread.sleep(2000);
        System.out.println(Test.a);
    }

}

这个输出结果是不固定的,第一次可能是 98 ,第二次可能是 97 ,这个结果就和我们预期的结果不一致(预期结果是100),所以一个对象是否是线程安全的,取决于它是否会被多个线程访问,以及程序中是如何去使用这个对象的。如果 多个线程访问同一个共享对象,在不需额外的同步以及调用端代码不用做其他协调的情况下,这个共享对象的状态 依然是正确的(正确性意味着这个对象的结果与我们预期 规定的结果保持一致),那说明这个对象是线程安全的。

对于线程安全性,本质上是管理对于数据状态的访问,而且这个这个状态通常是共享的、可变的。

共享:是指这个 数据变量可以被多个线程访问;

可变:指这个变量的值在 它的生命周期内是可以改变的。

synchroinzed关键字

说到锁,我们通常会谈到synchronized这个关键字。它翻译成中文就是“同步”的意思。

我们通常使用synchronized关键字来给一段代码或一个方法上锁。它通常有以下三种形式:

  1. 修饰实例方法,锁是当前实例对象 ,进入同步代码前要获得当前实例的锁;
  2. 修饰静态方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁;
  3. 修饰代码块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
public class SynchroinzedDemo {

    /**
     * 对静态方法加锁
     */
    public static synchronized void test(){}
    /**
     * 对实例方法加锁
     */
    public synchronized void test1(){}
    /**
     * 对代码块加锁
     */
    public void test2(){
        synchronized(this){}
    }
}

对上面的代码加锁:

public class Test {

    public static int a = 0;

    public synchronized static void addA(){
        a++;
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
          new Thread(new Runnable() {
                @Override
                public void run() {
                    addA();
                }
            }).start();
        }
        Thread.sleep(2000);
        System.out.println(Test.a);
    }

}

经过多次运行,结果都是100,说明线程安全。

几种锁

Java 6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁“。在Java 6 以前,所有的锁都是”重量级“锁。所以在Java 6 及其以后,一个对象其实有四种锁状态,它们级别由低到高依次是:

  1. 无锁状态
  2. 偏向锁状态
  3. 轻量级锁状态
  4. 重量级锁状态

无锁就是没有对资源进行锁定,任何线程都可以尝试去修改它,无锁在这里不再细讲。

几种锁会随着竞争情况逐渐升级,锁的升级很容易发生,但是锁降级发生的条件会比较苛刻,锁降级发生在Stop The World期间,当JVM进入安全点的时候,会检查是否有闲置的锁,然后进行降级。

关于锁降级有两点说明:

1.不同于大部分文章说锁不能降级,实际上HotSpot JVM 是支持锁降级的,文末有链接。

2.上面提到的Stop The World期间,以及安全点,这些知识是属于JVM的知识范畴,本文不做细讲。

下面分别介绍这几种锁以及它们之间的升级。

Java对象头

在 JVM 中,对象在内存中分为三块区域:

  • 对象头

    • Mark Word(标记字段):默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
    • Klass Point(类型指针):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
  • 实例数据

    • 这部分主要是存放类的数据信息,父类的信息。
  • 对其填充

    • 由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。

      Tip:不知道大家有没有被问过一个空对象占多少个字节?就是8个字节,是因为对齐填充的关系哈,不到8个字节对其填充会帮我们自动补齐。

      img

偏向锁

偏向锁是JDK6中引入的一项锁优化,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。

轻量级锁

如果明显存在其它线程申请锁,那么偏向锁将很快升级为轻量级锁。

自旋锁

自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

重量级锁

指的是原始的Synchronized的实现,重量级锁的特点:其他线程试图获取锁时,都会被阻塞,只有持有锁的线程释放锁之后才会唤醒这些线程。

锁升级的场景

  • 场景1: 程序不会有锁的竞争。那么这种情况我们不需要加锁,所以这种情况下对象锁状态为无锁。

  • 场景2: 经常只有某一个线程来加锁。

    加锁过程:也许获取锁的经常为同一个线程,这种情况下为了避免加锁造成的性能开销,所以并不会加实际意义上的锁,偏向锁的执行流程如下:

  1. 线程首先检查该对象头的线程ID是否为当前线程;

  2. A:如果对象头的线程ID和当前线程ID一致,则直接执行代码;
    B:如果不是当前线程ID则使用CAS方式替换对象头中的线程ID,如果使用CAS替换不成功则说明有线程正在执行,存在锁的竞争,这时需要撤销偏向锁,升级为轻量级锁。

  3. 如果CAS替换成功,则把对象头的线程ID改为自己的线程ID,然后执行代码。

  4. 执行代码完成之后释放锁,把对象头的线程ID修改为空。

  • 场景3: 有线程来参与锁的竞争,但是获取锁的冲突时间很短。

当开始有锁的冲突了,那么偏向锁就会升级到轻量级锁;线程获取锁出现冲突时,线程必须做出决定是继续在这里等,还是回家等别人打电话通知,而轻量级锁的路基就是采用继续在这里等的方式,当发现有锁冲突,线程首先会使用自旋的方式循环在这里获取锁,因为使用自旋的方式非常消耗CPU,当一定时间内通过自旋的方式无法获取到锁的话,那么锁就开始升级为重量级锁了。

  • 场景4: 有大量的线程参与锁的竞争,冲突性很高。

我们知道当获取锁冲突多,时间越长的时候,我们的线程肯定无法继续在这里死等了,所以只好先休息,然后等前面获取锁的线程释放了锁之后再开启下一轮的锁竞争,而这种形式就是我们的重量级锁。

JVM 是如何实现 synchronized 的?

咱们先来看个 demo :

public class Demo {

    public void test(Object o){
        synchronized (o){

        }
    }
}

进入到 class 文件所在的目录下,使用 javap -v demo.class 来看一下编译的字节码(在这里我截取了一部分):

  public void test(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=2
         0: aload_1
         1: dup
         2: astore_2
         3: monitorenter
         4: aload_2
         5: monitorexit
         6: goto          14
         9: astore_3
        10: aload_2
        11: monitorexit
        12: aload_3
        13: athrow
        14: return
      Exception table:
         from    to  target type
             4     6     9   any
             9    12     9   any

应该能够看到当程序声明 synchronized 代码块时,编译成的字节码会包含 monitorentermonitorexit 指令,这两种指令会消耗操作数栈上的一个引用类型的元素(也就是 synchronized 关键字括号里面的引用),作为所要加锁解锁的锁对象。如果看的比较仔细的话,上面有一个 monitorenter 指令和两个 monitorexit 指令,这是 Java 虚拟机为了确保获得的锁不管是在正常执行路径,还是在异常执行路径上都能够解锁。

关于 monitorentermonitorexit ,可以理解为每个锁对象拥有一个锁计数器和一个指向持有该锁的线程指针:

  • 当程序执行 monitorenter 时,如果目标锁对象的计数器为 0 ,说明这个时候它没有被其他线程所占有,此时如果有线程来请求使用, Java 虚拟机就会分配给该线程,并且把计数器的值加 1;

    • 目标锁对象计数器不为 0 时,如果锁对象持有的线程是当前线程, Java 虚拟机可以将其计数器加 1 ,如果不是呢?那很抱歉,就只能等待,等待持有线程释放掉。
  • 当执行 monitorexit 时, Java 虚拟机就将锁对象的计数器减 1 ,当计数器减到 0 时,说明这个锁就被释放掉了,此时如果有其他线程来请求,就可以请求成功

为什么采用这种方式呢?是为了允许同一个线程重复获取同一把锁。 比如,一个 Java 类中拥有好多个 synchronized 方法,那这些方法之间的相互调用,不管是直接的还是间接的,都会涉及到对同一把锁的重复加锁操作。这样去设计的话,就可以避免这种情况。

小结

synchronized 关键字是通过 monitorenter 和 monitorexit 两种指令来保证锁的。

当一个线程准备获取共享资源时:

  • 首先检查 MarkWord 里面放的是不是自己的 ThreadID ,如果是,说明当前线程处于 “偏向锁”

  • 如果不是,锁升级,这时使用 CAS 操作来执行切换,新的线程根据 MarkWord 里面现有的 ThreadID 来通知之前的线程暂停,将 MarkWord 的内容置为空。

  • 然后,两个线程都将锁对象 HashCode 复制到自己新建的用于存储锁的记录空间中,接着开始通过 CAS 操作,把锁对象的 MarkWord 的内容修改为自己新建的记录空间地址,以这种方式竞争 MarkWord ,成功执行 CAS 的线程获得资源,失败的则进入自旋。

    • 自旋的线程在自旋过程中,如果成功获得资源(也就是之前获得资源的线程执行完毕,释放了共享资源),那么整个状态依然是 轻量级锁 的状态。
    • 如果没有获得资源,就进入 重量级锁 的状态,此时,自旋的线程进行阻塞,等待之前线程执行完成并且唤醒自己。

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

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

相关文章

御用飞场之惊险炸鸡寻根溯源

御用飞场之惊险炸鸡寻根溯源 1. 源由2. 分析3. 证据4. 总结5. 补充&#xff1a;BetaFlight Mark4 自锁螺母桨叶松动 炸机瞬间 1. 源由 这个炸鸡的原因千奇百怪&#xff0c;不过最终的结果都是相似的。 如果能很好的找到根原因&#xff0c;相对来说&#xff0c;今后炸鸡的概…

element-ui表格el-table的使用

先给大家展示一下效果 Table 属性 属性名说明类型可选值默认值data显示的数据array——heightTable 的高度&#xff0c; 默认为自动高度。 如果 height 为 number 类型&#xff0c;单位 px&#xff1b;如果 height 为 string 类型&#xff0c;则这个高度会设置为 Table 的 sty…

初阶指针(详解)

目录 前言 一 指针是什么 计算机又是如何编址的&#xff1f; 总结 二 指针和指针类型 指针-整数 总结&#xff1a; 指针的解引用 总结 三 野指针 概念 野指针的成因 1. 指针未初始化 2. 指针越界访问 3. 指针指向的空间被释放 如何规避野指针 四 指针运算…

iMazing2.16.9中文最新版iOS设备管理器下载教程

iMazing2.16.9是一款兼容Win和Mac的iOS设备管理软件。iMazing能够将音乐、文件、消息和应用等数据从任何 iPhone、iPad 或 iPod 传输到 Mac 或 PC 上。iMazing轻松管理和备份您的 iOS 设备,无需使用 iTunes&#xff0c;iMazing以自己的方式管理 iPhone。让备受信赖的软件为您传…

遗传算法(附简单案例及matlab详细代码)

作者&#xff1a;非妃是公主 专栏&#xff1a;《智能优化算法》 博客地址&#xff1a;https://blog.csdn.net/myf_666 个性签&#xff1a;顺境不惰&#xff0c;逆境不馁&#xff0c;以心制境&#xff0c;万事可成。——曾国藩 文章目录 专栏推荐序一、生物进化二、遗传算法原…

华为OD机试真题 Java 实现【按身高和体重排队】【2022Q4 100分】,附详细解题思路

一、题目描述 某学校举行运动会&#xff0c;学生们按编号(1、2、3…n)进行标识&#xff0c;现需要按照身高由低到高排列&#xff0c;对身高相同的人&#xff0c;按体重由轻到重排列&#xff1b; 对于身高体重都相同的人&#xff0c;维持原有的编号顺序关系。请输出排列后的学生…

全闪SDS三节点EC(4+2:1)性能挑战测试

前段时间咱们存储圈在讨论一下全闪SDS性能挑战&#xff1a; 三节点集群&#xff0c;用EC&#xff08;42:1&#xff09;&#xff0c;性能目标是&#xff1a;4KB随机读写7:3&#xff0c;达到100万IOPS&#xff0c;平均时延0.5ms&#xff0c;P99时延1ms。硬件配置&#xff1a;网络…

菜鸟的刷题之路之二叉树

&#x1f495;“成功不是终点&#xff0c;失败不是终结&#xff0c;勇气才是启程的第一步。”&#x1f495; &#x1f43c;作者&#xff1a;不能再留遗憾了&#x1f43c; &#x1f386;专栏&#xff1a;菜鸟的刷题之路&#x1f386; &#x1f697;本文章主要内容&#xff1a;将…

GORM---创建

目录 模型定义使用Create创建记录一次性创建多条数据批量插入数据时开启事务默认值问题 模型定义 定义一个PersonInfo结构体。 type PersonInfo struct {Id uint64 gorm:"column:id;primary_key;NOT NULL" json:"id"UserName string gorm:"co…

路径规划算法:基于狮群优化的路径规划算法- 附代码

路径规划算法&#xff1a;基于狮群优化的路径规划算法- 附代码 文章目录 路径规划算法&#xff1a;基于狮群优化的路径规划算法- 附代码1.算法原理1.1 环境设定1.2 约束条件1.3 适应度函数 2.算法结果3.MATLAB代码4.参考文献 摘要&#xff1a;本文主要介绍利用智能优化算法狮群…

Prometheus+Grafana(外)监控Kubernetes(K8s)集群(基于containerd)

一、实验环境 1、k8s环境 版本v1.26.5 二进制安装Kubernetes(K8s)集群(基于containerd)—从零安装教程&#xff08;带证书&#xff09; 主机名IP系统版本安装服务master0110.10.10.21rhel7.5nginx、etcd、api-server、scheduler、controller-manager、kubelet、proxymaster021…

在 Ubuntu 20.04 上安装 Nginx

保证以 sudo 用户身份登录&#xff0c;并且你不能运行 Apache 或者 其他处理进程在80端口和443端口。 安装 Nginx Nginx 在默认的 Ubuntu 源仓库中可用。想要安装它&#xff0c;运行下面的命令&#xff1a; sudo apt update sudo apt install nginx 一旦安装完成&#xff0…

Redis高级数据结构之Bitmaps

Bitmaps的介绍 现代计算机使用二进制位作为信息存储的基本单元。一个字节&#xff08;Byte&#xff09;等于8个二进制位&#xff08;bit&#xff09;。合理的使用位能有效提高内存使用率和开发效率。位是最小信息单位&#xff0c;可以表示两个状态之一。字节是更大的单位&…

虚拟机搭建

Linux(CentOS-7.6-x64位)基础配置, 虚拟机平台VmWare15 CentOS-7.6-x64镜像下载&#xff1a; https://www.aliyundrive.com/s/72Xg449t6i8 提取码: 32rm VmVare15安装包下载带序列号&#xff1a;VmVare15安装包下载带激活序列号资源-CSDN文库 点击关闭&#xff0c;点击完成&…

深入理解Jar文件:创建、使用和多版本控制

&#x1f9d1;‍&#x1f4bb;CSDN主页&#xff1a;夏志121的主页 &#x1f4cb;专栏地址&#xff1a;Java基础进阶核心技术专栏 目录 &#x1f35b; 一、创建JAR文件 &#x1f35c; 二、安装和使用清单文件 &#x1f35d; 三、创建可执行的JAR文件 &#x1f360; 四、多版…

微信小程序开发实战 ②②(全局数据共享)

作者 : SYFStrive 博客首页 : HomePage &#x1f4dc;&#xff1a; 微信小程序 &#x1f4cc;&#xff1a;个人社区&#xff08;欢迎大佬们加入&#xff09; &#x1f449;&#xff1a;社区链接&#x1f517; &#x1f4cc;&#xff1a;觉得文章不错可以点点关注 &#x1f4…

网络安全大厂面试题合集+

以下为网络安全各个方向涉及的面试题&#xff0c;星数越多代表问题出现的几率越大&#xff0c;祝各位都能找到满意的工作。 注&#xff1a;本套面试题&#xff0c;已整理成pdf文档&#xff0c;但内容还在持续更新中&#xff0c;因为无论如何都不可能覆盖所有的面试问题&#xf…

近年GDC服务器分享合集(三): 《Sky光·遇》实现百万在线:一种云原生的扩容方法

如今&#xff0c;游戏行业对于云原生技术的使用越来越广泛。特别是那些拥有海量玩家在线的游戏&#xff0c;使用云原生技术可以轻松做到高可用、弹性扩容和降低成本。在GDC 2022上&#xff0c;来自《Sky光遇》项目的工程师分享了相关的经验——《《Sky光遇》实现百万在线&#…

黑马Redis视频教程高级篇(一:分布式缓存)

目录 分布式缓存 一、Redis持久化 1.1、RDB持久化 1.1.1、执行时机 1.1.2、RDB原理 1.1.3、小结 1.2、OF持久化 1.2.1、AOF原理 1.2.2、OF配置 1.2.3、AOF文件重写 1.3、RDB与AOF对比 二、Redis主从 2.1、搭建主从架构 2.1.1、集群结构 2.1.2、准备实例和配置 …

基于springboot汽车站车辆运管系统java+vue

本汽车站车辆运管系统管理员可以管理个人中心&#xff0c;业务管理&#xff0c;站务管理&#xff0c;人力资源管理&#xff0c;办公司管理&#xff0c;财务管理。因而具有一定的实用性。本站是一个B/S模式系统&#xff0c;采用springboot框架&#xff0c;MYSQL数据库设计开发&a…