synchronized详解

news2024/12/25 8:51:37

什么是线程安全问题?

简单来说就是当多个线程同时访问某个方法时,这个方法无法按照我们预期的行为来执行,那么我们认为这个方法是线程不安全的

导致线程不安全的主要原因

1.原子性

什么是原子性

比如在数据库事务的ACID特性中 当前操作中包含多个数据库事务操作要么同时成功要么同时失败 不允许存在部分成功部分失败的情况
而在多线程中的原子性与数据库的原子性相同 它是指一个或者多个指令操作在CPU执行过程中不允许被中断

案例

public class ThreadDemo {

    private int i = 0;
    public void incr() {
        i++;
    }

    public static void main(String[] args) throws Exception {
        ThreadDemo threadDemo = new ThreadDemo();
        Thread[] threads = new Thread[2];
        for (int k = 0; k <2 ; k++) {
            threads[k] = new Thread(()->{
                for (int j = 0; j<10000 ; j++) {
                    threadDemo.incr();
                }
            });
            threads[k].start();
        }
        threads[0].join();
        threads[1].join();
        System.out.println(threadDemo.i);
    }
}

以上案例同时启动两个线程 每个线程对i进行++正常情况下 i应该=20000 但是打印出来的数值却小于两万 和预期结果不一致导致这一现象发生的原因就是原子性问题

上述案例执行过程分析

1.线程1先获得CPU执行权 在CPU将i=0加载到寄存器 中后出现线程切换 CPU将执行权切换给线程2并保留当前的CPU上下文
2.线程2同样去内存中将i加载到寄存器中进行计算 然后把计算结果写回内存
3.线程2释放了CPU资源 线程1重新获得执行权后恢复CPU上下文而这时i=0
4.最后计算i的结果比预期小‘
请添加图片描述

产生原子性问题的原因

1.CPU时间片切换
2.执行指令的原子性(线程运行的程序或者指令是否具有原子性)

原子性问题的解决方法

1.保证i++操作在执行过程中不存在上下文切换
2.互斥条件保证串行执行
在Java中 提供了一种解决方案 互斥锁 synchronized
以下代码只要在incr方法上加上synchronized 关键字就可使得运行结果一定=20000

public class ThreadDemo {

    private  int i = 0;
    public synchronized void incr() {
        i++;
    }

    public  static void main(String[] args) throws Exception {

        ThreadDemo threadDemo = new ThreadDemo();
            Thread[] threads = new Thread[2];
            for (int k = 0; k <2 ; k++) {
                threads[k] = new Thread(()->{
                    for (int j = 0; j<10000 ; j++) {
                        threadDemo.incr();
                    }
                });
                threads[k].start();
            }
            threads[0].join();
            threads[1].join();
            System.out.println(threadDemo.i);

    }
}

2.有序性

volatile 的时候在详细说

3.可见性

volatile 的时候在详细说

synchronized的作用

以上示例中得出结论导致线程安全的根本原因在于 多个线程同时操作共享资源就需要保证共享资源的独占性 在Java中提供了synchronized关键字 他能保证同一时刻只有一个线程执行该代码块
synchronized是互斥锁 相当于把并行的线程变成了串行执行 但是也牺牲了性能

synchronized的作用范围

在synchronized中提供了类锁和对象锁

类锁

类锁是全局锁 当多个线程调用不同实例对象的同步方法时会产生互斥

    public static synchronized  void  incr() {
        ......
    }
        synchronized (ThreadDemo.class) {
            ......
        }
@Slf4j
public class ThreadDemo {

    private    int i = 0;
    public  void incr(){
      synchronized (ThreadDemo.class) {
          while (true) {
              log.info("当前访问线程"+Thread.currentThread().getName());
              try {
                  Thread.sleep(100);
              }catch (Exception e) {
                  e.fillInStackTrace();
              }

          }
      }
    }

    public  static void main(String[] args) throws Exception {

        ThreadDemo threadDemo = new ThreadDemo();
        ThreadDemo threadDemo1 = new ThreadDemo();
        new Thread(()->threadDemo.incr(),"threadDemo").start();
        new Thread(()->threadDemo1.incr(),"threadDemo1").start();
    }

}

根据类锁的范围 即使有多个对象实例 哪个线程抢到了锁 哪个线程就打印自己的线程名称

对象锁

当多个线程用同一个对象实例的同步方法时会产生互斥

    public  synchronized void incr(){
       ......
    }
    private Object lock = new Object ();

    public   void incr(){
          synchronized (lock) {
              ......
          }
    }
@Slf4j
public class ThreadDemo {


    private Object lock = new Object();

    public  void incr(){
      synchronized (lock ) {
          while (true) {
              log.info("当前访问线程"+Thread.currentThread().getName());
              try {
                  Thread.sleep(100);
              }catch (Exception e) {
                  e.fillInStackTrace();
              }

          }
      }
    }

    public  static void main(String[] args) throws Exception {

        ThreadDemo threadDemo = new ThreadDemo();
        ThreadDemo threadDemo1 = new ThreadDemo();
        new Thread(()->threadDemo.incr(),"threadDemo").start();
        new Thread(()->threadDemo1.incr(),"threadDemo1").start();
    }

}
16:34:02.861 [threadDemo] INFO com.alipay.alibabademo.thread.ThreadDemo - 当前访问线程threadDemo
16:34:02.861 [threadDemo1] INFO com.alipay.alibabademo.thread.ThreadDemo - 当前访问线程threadDemo1
16:34:02.964 [threadDemo1] INFO com.alipay.alibabademo.thread.ThreadDemo - 当前访问线程threadDemo1
16:34:02.964 [threadDemo] INFO com.alipay.alibabademo.thread.ThreadDemo - 当前访问线程threadDemo
16:34:03.078 [threadDemo] INFO com.alipay.alibabademo.thread.ThreadDemo - 当前访问线程threadDemo

我们可以看到 相同的代码 在使用对象锁的情况下 当两个不同实例的方法执行时 并没有达到互斥的目的 根源在于 synchronized (lock )锁的范围太小
Class是在JVM启动过程中加载的 每个.class文件 被装载之后会产生一个class对象 class在JVM进程中是唯一的 通过static修饰的成员变量及方法的声明周期都是类级别的 会随着类的定义被分配到内存中 随着类的卸载被回收掉

而对象的生命周期随着实例对象的创建而开始 随着实例对象的回收而回收
因此类锁和对象锁最大的区别是锁对象的声明周期不一样 如果要达到互斥那么就需要多个线程竞争同一个对象锁

synchronized原理实现

同步锁设计猜想

1.同步锁的特性就是排他互斥 要达到这个目的 多个线程就必须去争抢同一个资源
2.在同一时刻当一个线程获取到了这个资源那么其他资源就必须要去等待
3.处于等待的线程肯定不能去一致占用CPU
4.如果很多的线程被阻塞 那么是否可以设计一个容器去装这些阻塞的线程 当获得锁的线程运行结束之后在去容器里面唤醒线程 被唤醒的线程再去尝试抢占锁
请添加图片描述

同步标记如何存储的?

通过上述我们可以猜想 有一个互斥的标记来记录是否已经有线程获取到了锁
如果有的话那其他的线程就需要去阻塞等待 那么这个互斥标记是怎么存储的呢?

Mark Word的存储结构

一个Java对象被初始化之后会被储存在堆内存中 那么这个对象结构又是如何呢?
Java对象的存储结构可以分为三个部分对象头、实例数据、对象填充
在这里插入图片描述

对象头

对象头又由三个部分组成

Mark Word

Mark Word 记录了对象和锁相关的信息,当这个对象作为锁对象来实现synchronized的同步操作时锁标记和相关信息都存储在Mark Word中
不同锁状态下32位Mark Word的结构信息
在这里插入图片描述
不同锁状态下64位Mark Word的结构信息
在这里插入图片描述
不管是32还是64位系统中 Mark Word中都会包含GC年龄分代 锁状态标记 hashCode等信息。从图中可以看到一个锁状态字段 它包含的状态 分别是 无锁,偏向锁,轻量级锁,重量级锁 。

Klass Pointer

表示指向类的指针 JVM通过这个指针来确认对象具体属于哪个类的实例

Length

表示数组长度

实例数据

实例数据就是所有类的成员变量 比如一个对象中包含 int、long等类型成员变量这些成员变量就存储在实例数据中

对齐填充

其目的是使得当前对象实例占用的存储空间时8字节的倍数 如果一个对象实例不是8字节的倍数会使用对齐填充
为了减少CPU访问内存的频率 从而达到性能提升的效果所以设计的8的倍数

对象实际存储图解

请添加图片描述

synchronized的锁类型

1.偏向锁
2.轻量级锁
3.重量级锁
在JDK1.6之前synchronized只提供了重量级锁的机制 JDK1.6之后synchronized做了很多优化 针对锁类型增加了偏向锁和轻量级锁 这两种锁的核心设计理念就是如何让线程在不阻塞的情况下达到线程安全的目的

偏向锁的原理分析

偏向锁会在线程没有竞争的情况下去访问synchronized同步代码块时会先尝试通过偏向锁来抢占访问资源 这个抢占过程是基于CAS完成的 如果抢占成功 则直接修改对象头中的锁标记 其中偏向锁标记为1 锁标记为01 以及存储当前线程的ID ,而偏向的意思就是如果线程X获得了偏向锁 那么当线程X后续在访问这个同步方法时只需要判断对象头中的线程ID和线程X是否相等即可 如果相等不需要再去抢占锁 直接获取访问资格

请添加图片描述
偏向锁是没有线程竞争情况下实现的一把锁 但是不能排除存在锁竞争的情况 所以偏向锁获取分为两种情况

1.没有锁竞争的情况

1.在没有锁竞争的时候 线程1访问同步代码块时从当前线程的栈中找到一个空闲的Basic ObjectLock 锁对象
2.将BasicObjectLock中的oop指针指向当前的锁对象lock
3.获得当前锁对象的对象头 通过对象头来判断是否可偏向 也就是说是锁标记为01 并且Thread为空
4.如果为可偏向状态 那么判断当前偏向的线程是不是线程1 如果偏向的是自己 则不需要在抢占锁 直接可以运行代码块
5.如果为不可偏向状态 则需要通过轻量级锁来实现锁的抢占过程‘
6.如果对象锁lock偏向其他线程或者没有偏向任何一个线程 则先构建一个匿名偏向的MarkWord 然后通过CAS方法把一个匿名偏向的Mark Word修改为偏向线程1 如果当前锁对象lock 已经偏向其他线程 那么CAS一定会失败’

2.有锁竞争的情况

假设线程1获得了偏向锁 此时线程2去执行代码块代码 如果访问的是同一个对象锁 则会触发锁竞争 并触发偏向锁撤销进行如下流程
1.线程2调用撤销偏向锁方法 尝试撤销lock锁对象的偏向锁
2.撤销偏向锁需要到达一个安全点才能执行 当到达安全点后 会暂停或的偏向锁的线程1
3.检查获得偏向锁的线程1
如果线程1还在执行同步代码块中的指令 直接把锁对象lock升级成轻量级锁 并且指向线程1 表示线程1获得轻量级锁
如果线程1执行完了代码块中的指令 或者处于非活的状态下 直接把偏向锁撤销恢复无锁状态 然后线程2升级成轻量级锁 通过轻量级锁获取资源

偏向锁的释放

把lock Record释放锁对象的Mark Word释放为空

轻量级锁原理分析

在线程没有竞争时,使用偏向锁能够在不影响性能的前提下获得锁资源,但是同一时刻只允许一个线程获得锁资源 如果突然有多个线程来访问同步方法,那么没有抢占到锁资源的线程 进行一定次数的重试(自旋) 。比如线程第一次没抢到锁则重试几次,如果在重试的过程中抢占到了锁 那么线程就不需要阻塞这种实现方式称为自旋锁
请添加图片描述
如果一直自旋来重试抢占锁的方式是有代价的,cpu一直处于运行状态 如果持有锁的线程占有锁的时间比较短 那么自旋等待带来的性能提升比较明显,如果持有锁的线程占用资源时间较长 那么自旋的线程就会浪费CPU资源所以重试必须有一个限制
在JDK1.6中默认自旋次数是10次 并且可以通过-XX:PreBlockSpin参数来调整自旋次数 同时JDK1.6还对自旋进行了优化引入了自适应自旋,自适应自旋的次数不是固定的而是根据上次同一个锁上的自旋次数以及持有者的状态决定的 如果在同一个锁对象上通过自旋等待成功获得过锁 并且持有锁的线程正在运行中 那么JVM会认为此次自旋也有很大机会获得锁 因此会将这个线程的自旋时间相对延长 反之如果一个锁对象 通过自旋获得锁很少成功 那么JVM会缩短自旋次数

重量级锁的原理分析

轻量级锁能够通过一定次数的重试让没有获得锁的线程有可能抢占到锁资源 但是轻量级锁只能在获得锁的线程持有锁的时间较短的情况下才能起到提升同步锁性能的效果 如果持有锁线程占用资源的时间较长,那么不能让那些没有抢占到锁的自旋不断自旋 否则会占有过多的CPU资源
如果没抢占到锁的线程通过一定次数的自旋后 发现仍然没有抢占到锁就只能阻塞等待 所以最终会升级到重量级锁 通过系统层面的互斥量来抢占资源

锁升级的流程

请添加图片描述
当一个线程访问增加了synchronized关键字代码块时 如果偏向锁是开启状态 则先尝试通过偏向锁来获得锁资源 这个过程用CAS完成 如果当前已经有其他线程获得了偏向锁 那么抢占锁资源的线程由于无法获得锁 所以会尝试升级到轻量级锁来进行锁资源抢占轻量级锁就是通过多次CAS来完成的 如果这个线程通过多次自旋仍然无法获得锁资源 那么最终只能升级到重量级锁实现锁等待

偏向锁实现原理

偏向锁就是使用CAS机制来替换对象头中的ThreadId如果成功则获得偏向锁 否则就会升级到轻量级锁

轻量级锁的实现原理

如果偏向锁存在竞争或者偏向未开启那么当线程访问synchronized同步代码块时会采用轻量级锁来抢占锁资源 获得访问资格

轻量级锁流程

请添加图片描述
1.线程2进入同步代码块之后 JVM会给当前线程分配一个Lock Record(Basic Obejct Lock对象) 它的成员对象BasicLock中有一个成员属性markOop_displaced_header专门用来保存锁对象lock的原始Mark Word
2.构建一个无锁状态的Mark Word(lock锁对象的mark Word 但是锁状态是无锁) 把这个无锁状态的Mark Word设置到Lock Record 中的_displaced_header字段中
3.通过cas 将lock 锁对象的Mark Word 替换为指向Lock Record的指针 如果替换成功 表示轻量级锁抢占成功线程2可以执行代码块
4.如果cas失败 则说明当前lock锁对象不是无锁状态 会触发锁膨胀 升级成重量级锁

轻量级锁的释放

1.采用cas把Lock Record中——displaced_header存储的lock锁对象的Mark Word替换到lock锁对象的MarkWord中
2.如果替换成功则轻量级锁释放完成
3.如果替换失败 说明释放锁的时候发生了竞争 触发锁膨胀 在调用重量级锁的释放锁的方式完成锁的释放

重量级锁的实现原理

线程在执行代码块的时候 发现锁状态是轻量级锁 并且有线程抢占了锁资源 当自旋到一定次数之后该线程会触发锁膨胀会膨胀成重量级锁 因此重量级锁是在锁竞争的场景下

请添加图片描述

重量级锁的释放

1.把ObjectMonitor中持有锁的对象——owner设置为null。
2.从_cxq队列中唤醒一个处于阻塞队列中的线程。
3.被唤醒的线程重新竞争重量级锁 由于synchronized是非公平锁,所以不一定被释放的锁就能抢到锁资源。

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

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

相关文章

DolphinDB 诚挚招募实施伙伴

随着 DolphinDB 业务发展&#xff0c;为满足迅速增长的客户需求&#xff0c;我们现正式启动“实施伙伴招募计划”。DolphinDB 客户已经涵盖7家Top 10券商、头部公募及私募基金、知名银行、交易所、世界500强制造业客户、标杆能源企业等&#xff0c;我们非常期待和欢迎实施伙伴们…

毕业设计 基于stm32的智能平衡小车 - 单片机 物联网嵌入式

文章目录0 前言1 项目背景2 设计思路3 硬件设计4 软件设计4.1 核心PID控制4.2 直立控制程序设计4.3 速度控制程序设计4.4 方向控制程序设计4.5 关键代码5 最后0 前言 &#x1f525; 这两年开始毕业设计和毕业答辩的要求和难度不断提升&#xff0c;传统的毕设题目缺少创新和亮点…

32、Java高级特性——日期操作类、Date类、SimpleDateFormat类、Calendar类

目录 一、日期操作类 二、Date类 1、Date类中的构造方法 2、获取系统当前时间 三、SimpleDateFormat类 1、SimpleDateFormat类中的构造方法 2、format(Date date,StringBuffer toAppendTo,FieldPosition pos)方法 四、Calendar类 1、Calendar类中的构造方法 2、Cal…

[附源码]Python计算机毕业设计SSM家政信息管理平台(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Removing the Bias of Integral Pose Regression 阅读笔记

消除积分姿态回归的偏差 ICCV2021 论文链接 补充材料链接 参考链接 摘要&#xff1a; 尽管姿态估计的回归法更直观&#xff0c;但由于热图法的优越性能&#xff0c;使其在2D人体姿态估计中占主导地位。积分回归法在架构上使用隐式热图&#xff0c;拉近了热图法与回归法。这就…

GraalVM + Springboot3 + IDEA 在widow10 上完成构建本地化服务

GraalVM是开发人员编写和执行Java代码的工具。具体来说&#xff0c;GraalVM是由Oracle创建的Java虚拟机&#xff08;JVM&#xff09;和Java开发工具包&#xff08;JDK&#xff09;。它是一个高性能的运行时&#xff0c;可以提高应用程序的性能和效率。 GraalVM的目标包括&…

【数据结构】哈希表

目录 一、哈希函数的引入 二、解决哈希冲突的思路 2.1基于闭散列的思路 2.2基于开散列的思路 2.3负载因子 三、关于哈希函数的设计 四、基于拉链法实现哈希表 4.1哈希表的内部构造 4.2插入操作 4.3扩容操作 4.4搜索操作 4.5删除操作 哈希表其实就是基于数组衍生而来…

深度解读面试题:链表中环的入口结点(附代码,可过在线OJ)

在解读“链表中环的入口结点”前&#xff0c;我认为有必要明白关于它的一些用于打基础的问题&#xff08;相交链表、判断链表中是否存在环&#xff09; 相交链表 题目&#xff1a; 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点…

快收藏!!整理了100个Python小技巧!!

下面&#xff0c;我就给大家分享100个Python小技巧&#xff0c;帮助大家更好的了解和学习Python&#xff0c;欢迎收藏、关注&#xff0c;点赞支持&#xff01; ▍1、for循环中的else条件 这是一个for-else方法&#xff0c;循环遍历列表时使用else语句。下面举个例子&#xff…

根据平均值列出记录

AANSI SQL包括几个聚合函数&#xff0c;使您可以对一组值进行计算以将其结果作为单个值返回。他们包括Count(), Min(), Max(), Sum() and AVG(),以及其他。默认情况下&#xff0c;聚合函数适用于所有行&#xff0c;但是您可以通过将WHERE子句应用于SELECT语句来缩小字段的范围。…

直冲云霄,阿里大牛耗时49天整理12W字面试手册,押题准确率直冲95%

很多人都想进字节做开发&#xff0c;不论是技术还是薪资、福利都算得上TOP级~ 7月底官方再次启动扩招&#xff0c;发布了1200&#xff0b;后端工程师岗位&#xff01; 那么本批有哪些优质岗位可选择&#xff1f;薪资待遇如何&#xff1f; 下面给大家列出几类具体的岗位要求&a…

软件项目管理指南:定义、5大过程、估算及进度管理方法等

本文将分享&#xff1a;1、软件项目管理的定义&#xff1b;2、软件项目管理的过程步骤&#xff1b;3、软件项目管理的内容&#xff1b;4、软件项目估算与进度管理方法&#xff1b;5、软件开发各生命周期阶段与文档、角色间的关系&#xff1b;6、软件开发项目中的各大角色职能&a…

深度学习-第P1周——实现mnist手写数字识别

深度学习-第P1周——实现mnist手写数字识别深度学习-第P1周——实现mnist手写数字识别一、前言二、我的环境三、前期工作1、导入依赖项并设置GPU2、导入数据集3、数据可视化四、构建简单的CNN网络五、训练模型1、设置超参数2、编写训练函数3、编写测试函数4、正式训练六、结果可…

ADSP-21489的图形化编程详解(7:延时、增益、分频、反馈、响度)

延时 21489 可以做延时&#xff0c;音频高手会运用此项算法来增强音效&#xff0c;我们做个最简单的&#xff0c;让大家知道怎么用它&#xff0c;至于怎么样嵌入到自己的系统里实现更好的效果&#xff0c;则需要各位调音师专业的耳朵来判断&#xff0c;调音无上限&#xff01;…

MySQL之索引及其背后的数据结构

✨博客主页: 荣 ✨系列专栏: MySQL ✨一句短话: 难在坚持,贵在坚持,成在坚持! 文章目录一. 索引的介绍1. 什么是索引2. 索引的使用二. 索引背后的数据结构1. 考虑使用哈希表2. 二叉搜索树3. N叉搜索树(B树, B树)4. 注意事项一. 索引的介绍 1. 什么是索引 索引 (Index) 是帮助…

[激光原理与应用-39]:《光电检测技术-6》- 光干涉的原理与基础

目录 第1章 概述 1.1 什么是光干涉 1.2 产生干涉的必要条件 1.3 非相干光 - 自发辐射无法产生干涉 1.4 相干光 - 受激辐射 1.5 时间相干性 1.6 空间相干性 它山之石 第1章 概述 1.1 什么是光干涉 它是指因两束光波相遇而引起光的强度重新分布的现象。 指两列或两列以上…

Verilog入门学习笔记:Verilog基础语法梳理

无论是学IC设计还是FPGA开发&#xff0c;Verilog都是最基本、最重要的必备技能。但任何一门编程语言的掌握都需要长期学习。并不是简简单单的随便读几本书&#xff0c;随便动动脑筋那么简单。Verilog是一门基于硬件的独特语言&#xff0c;由于它最终所实现的数字电路&#xff0…

基于AVDTP信令分析蓝牙音频启动流程

前言 公司项目edifier那边需要在原来音频SBC,AAC基础上增加LHDC5.0编码&#xff0c;在打通lhdc协议栈之前&#xff0c;学习记录一番AVDTP音频服务流程。 一、AVDTP音频流基础知识 分析音频流程首先应具备的最简单基础概念知识&#xff1a;AVDTP信令signal&#xff0c;流端点se…

【JVM】垃圾回收机制详解(GC)

目录一.GC的作用区域二.关于对象是否可回收1.可达性分析算法和引用计数算法2.四种引用类型三.垃圾收集算法1.标记-清除算法2.复制算法3.标记-整理算法4.分代收集算法四.轻GC(Minor GC)和重GC(Full GC)一.GC的作用区域 可以看jvm详解之后&#xff0c;再来理解这篇文章更好 堆和…

[附源码]计算机毕业设计农村人居环境治理监管系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…