synchronized原理详解

news2024/12/22 17:35:01

众所周知,使用多线程可以极大地提升程序的性能,但如果多线程使用不合理,也会带来很多不可控的问题,例如线程安全问题。

什么是线程安全问题呢?如果多个线程同时访问某个方法时,这个方法无法得到我们预期的结果,那么就认为这个方法是线程不安全的。

1.经典案例:num++线程安全问题

先来看个线程不安全的示例:

public class UnSafeTest {
    @SneakyThrows
    public static void main(String[] args) {
        CountDownLatch countDownLatch = new CountDownLatch(10);
        CustomService customService = new CustomService(countDownLatch);
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    customService.add();
                }
                countDownLatch.countDown();
            }).start();
        }
        // 等待其他线程执行完毕
        countDownLatch.await();
        System.out.println("num:" + customService.getNum());
    }

    static class CustomService {
        private int num = 0;

        public void add() {
            num++;
        }

        public int getNum() {
            return num;
        }
    }
}

在上述代码中启动了10个线程,每个线程使得变量num累加100次,期望结果是1000,但打印结果却始终小于1000,这是一个典型的线程安全问题。

我们在多线程环境下调用了CustomService.add(),因而导致线程不安全,那么为什么会有线程安全问题呢?我们来看关键代码:

public void add() {
    num++;
}

num++,咋一看是不是觉得就是一个指令呀,但是实际上并不是我们想的那样。这就需要我们以字节码信息来分析了。

可以使用javap -v xx.class进行查看。

 0 aload_0
 1 dup
 2 getfield #2 <com/example/demo/thread/UnSafeTest$CustomService.num : I>
 5 iconst_1
 6 iadd
 7 putfield #2 <com/example/demo/thread/UnSafeTest$CustomService.num : I>
10 return

可以发现num++实际上由3个指令组成getfieldiaddputfield组成。

  • getfield:获取变量值。
  • iadd:执行+1。
  • putfield:设置变量值。

既然num++实际上由3个字节码指令组成,那么在多线程环境中就无法保证其执行过程的原子性

上面这种情况是原子性问题导致线程不安全,其实导致线程不安全的原因主要有3个:原子性可见性有序性,下面我们来看看它们的定义:

1)原子性

原子性主要保证一个或多个指令在执行过程中不允许被中断。

2)可见性

可见性主要保证一个线程对共享变量修改后,其他的线程立即能看到修改后的新值

3)有序性

有序性主要保证单线程下程序运行结果的正确性,即使编译器和处理器为了优化性能会对指令进行重排序

那么怎么解决线程安全问题呢?在Java中,我们可以使用synchronized关键字来解决线程安全问题中的原子性可见性有序性

2.synchronized解决线程安全问题

synchronized是一个同步锁,在同一时刻,被修饰的方法或代码块只有一个线程能执行,以保证线程安全。很多人都称之为重量级锁,但是,随着JDK1.6对synchronized进行了各种优化之后,在某些场景下它就并不那么重了。

既然synchronized可以帮助我们解决线程安全问题,那么到底怎么使用呢?话不多说,上代码:

public synchronized void add() {
    num++;
}

我们只是在add()方法上添加了synchronized关键字,就能够保证多线程环境下的线程安全了。当然这只是synchronized关键字的一种使用方式。我们来看看synchronized关键字的所有使用方式吧。

2.1 synchronized使用方式

2.1.1 修饰实例方法

synchronized修饰实例方法,锁对象为当前实例对象,也称之为对象锁,进入同步代码需要获取当前实例对象的锁。进入同步实例方法时,如果锁对象不是同一实例对象,则不会形成资源竞争,线程之间互不影响。

// 修饰实例方法
public synchronized void test1() {
    // 省略代码
    ......
}

多线程调用同一锁对象的不同实例方法。

示例代码:

public class SynchronizedTest {
    public static void main(String[] args) {
        CustomService customServiceA = new CustomService();
        new Thread(() -> customServiceA.test1(), "ThreadA").start();
        new Thread(() -> customServiceA.test2(), "ThreadB").start();
    }

    static class CustomService {
        // synchronized修饰实例方法
        @SneakyThrows
        public synchronized void test1() {
            TimeUnit.SECONDS.sleep(1);
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " method:test1 :" + i);
            }
        }

        // synchronized修饰实例方法
        @SneakyThrows
        public synchronized void test2() {
            TimeUnit.SECONDS.sleep(1);
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " method:test2 :" + i);
            }
        }
    }
}

执行结果:

虽然多个线程分别调用了不同的Synchronized修饰的实例方法,但是这些方法的锁对象是同一个,因此形成资源竞争,同一时刻只能有一个线程执行同步代码。

2.1.2 修饰静态方法

synchronized修饰静态方法,锁对象为当前类的Class对象,也称之为类锁,进入同步代码前需要获取类的Class对象的锁。

// 修饰静态方法
public static synchronized void test3() {
    // 省略代码
    ......
}

示例代码:

public class SynchronizedTest {
    @SneakyThrows
    public static void main(String[] args) {
        CustomService customServiceA = new CustomService();
        new Thread(() -> customServiceA.test1(), "ThreadA").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> customServiceA.test2(), "ThreadB").start();
    }

    static class CustomService {
        // 修饰静态方法
        @SneakyThrows
        public static synchronized void test3() {
            TimeUnit.SECONDS.sleep(2);
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " method:test3 :" + i);
            }
        }

        // 修饰实例方法
        @SneakyThrows
        public static void test4() {
            TimeUnit.SECONDS.sleep(1);
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " method:test4 :" + i);
            }
        }
    }
}

执行结果:

ThreadA先获得CustomService的Class对象锁,这是一个类锁(全局锁),而后ThreadB来获取CustomService的实例对象锁,虽然不是同一个锁对象,但是类锁是一个全局锁,因此形成资源竞争,同一时刻只能有一个线程执行同步代码。

2.1.3 修饰代码块

synchronized修饰代码块,取决于锁对象,对给定对象加锁,进入同步代码则需要获取给定对象的锁。

public void test5() {
    // 修饰代码块,锁对象为实例对象
    synchronized (this) {
		// 省略代码
        ......
    }
}

public void test6() {
    // 修饰代码块,锁对象为实例对象
    synchronized (new Object()) {
		// 省略代码
        ......
    }
}

public void test7() {
    // 修饰代码块,锁对象为类的Class对象
    synchronized (Object.class) {
		// 省略代码
        ......
    }
}

synchronized修饰代码块,重点在于锁对象,锁对象为实例对象时,其效果与修饰实例方法一致,锁对象为类的Class对象时,其效果与修饰静态方法一致。

2.2 synchronized锁标记存储

到这里,我们已经知道锁对象可以是实例对象,也可以是类的Class对象,其实这个锁对象就是线程竞争的资源,那么怎么标记某线程获得了该锁,从而使得其他线程无法同时获得该锁,这就需要我们了解堆中对象的存储结构

我们知道,一个Java对象被初始化之后会存储在堆内存中,那么在堆中存储了对象的哪些信息呢?

Java对象存储结构分为3个部分:对象头实例数据对齐填充

从上图中可以看到,对象头中Mark Word存储了锁相关的信息,我们来看看Mark Word的存储结构。

内置锁状态锁标记位(2bit)是否偏向锁(1bit)存储内容
无锁010对象哈希码、GC分代年龄
偏向锁011偏向线程id,偏向时间戳、对象分代年龄
轻量级锁00锁记录(Lock Record)指针
重量级锁10重量级锁指针
GC标记11

我们知道2bit最多只能表示四种状态:00,01,10,11,因此Mark Word额外通过1bit来表示无锁和偏向锁,0表示无锁,1表示偏向锁。

从表格得知,Mark Word的存储结构会随着内置锁状态的变化而变化,这也是我们需要注意的地方。

2.3 synchronized的锁升级

从堆中对象的存储结构,我们可以知道,Mark Word中存储了内置锁状态,这正是synchronized所依赖的,那么为什么会有这几种状态呢?

前面我们提到过,很多人称synchronized是重量级锁,性能不好,其实这种说法并不完全正确。

在JDK1.6之前synchronized确实是一个重量级锁,没有获得锁的线程会被阻塞,由用户态切换到内核态,这样切换的性能开销是非常大的。

因此,JDK1.6之后对synchronized做了很多优化,引入了偏向锁轻量级锁,使得某些场景下,锁就不那么重了(让线程在不阻塞的情况下达到线程安全)。

那么到底是怎么优化的呢?我们来看看偏向锁、轻量级锁、重量级锁的定义,这个疑问就不言而喻了。

2.3.1 偏向锁

在单线程环境下,访问synchronized修饰的同步代码,这个时候的锁状态就是偏向锁。

对于这个定义,可能大家会有疑问,既然是单线程环境,没有线程竞争,为什么还要加偏向锁呢?

因为,在实际开发中,加锁是为了避免出现线程安全问题,是否存在线程竞争是由应用场景决定的。假设存在这种情况,没必要一上来就使用重量级锁,这样显然很消耗性能。

我们来看看获得偏向锁的过程,自然就能明白其出现的必要性。

1)在没有线程竞争的情况下,线程A去访问Synchronized修饰的方法/代码块。

2)尝试通过偏向锁来获取锁资源(基于CAS)。

3)如果获取锁资源成功,则修改Mark Word中的锁标记,偏向锁标记为1,锁标记为01,并存储获得锁资源的线程id,然后执行代码。

4)如果同一线程再来访问,直接获取锁资源,然后执行代码。

2.3.2 轻量级锁

在没有线程竞争的场景下,使用偏向锁只允许同一时刻一个线程获取锁资源,如果这时有其他的线程来访问同步代码,没有获取到线程资源的线程应该怎么处理呢?

显然偏向锁无法解决这个问题,如果直接按照重量级锁的逻辑来解决,没有获取到锁资源的线程阻塞等待,必然会造成很大的性能消耗,于是乎,轻量级锁就出现了,偏向锁升级为轻量级锁。

所谓的轻量级锁,就是未获取到锁资源的线程,进行一定次数的自旋,重新尝试获取锁,如果在重试过程中获取到锁资源,那么此线程就不需要阻塞。

我们来看看获取轻量级锁的过程:

1)线程A已获取偏向锁。

2)线程B开始竞争锁资源,锁对象的线程id与线程B的线程id不一致,意味着出现锁竞争

3)在线程B的栈帧中创建锁记录Lock Record,用于存储锁对象的Mark Word旧信息以及锁对象地址,并将Mark Word中的信息,例如对象哈希码、GC分代年龄拷贝到Lock Record中的Displaced Mark Word中,以便后续锁释放时使用。

4)自旋尝试将Mark Word中锁指针记录更新为线程B栈帧中Lock Record的地址。

5)如果更新成功,则修改Mark Word中锁标记修改为00,偏向锁标记为0,并将Lock Recordowner指针指向当前锁对象。

Tips:

自旋重试过程中,会一直占用CPU资源,如果持有锁的线程占用锁资源的时间比较短,自旋会明显地提升性能,如果持有锁的线程占用锁资源的时间比较长,那么自旋就会浪费CPU资源,因此需要限制线程自旋的次数。

在JDK 1.6中默认的自旋次数是10次,我们可以通过**-XX:PreBlockSpin**参数来调整自旋次数。同时还引入的自适应自旋锁,来解决“锁竞争时间不确定”的问题,尽可能减少自旋次数。

2.3.3 重量级锁

轻量级锁能够让没能获取锁资源的线程进行一定次数的自旋重试有机会获取到锁资源,但如果持有锁的线程占用锁资源的时间较长,总不能让那些自旋了一定次数还是没有获取到锁资源的线程一直自旋下去吧,这样反而会占用很多的CPU资源,唯一的解决办法就是让这些线程阻塞等待,最终轻量级锁—》重量级锁。

重量级锁依赖于系统层面的Mutex Lock,会使线程阻塞,并由用户态转为内核态,这种切换的性能开销非常大。

2.3.4 锁升级过程

因此,锁的状态可以为无锁、偏向锁、轻量级锁、重量级锁。锁的级别从低到高为:无锁—》偏向锁—》轻量级锁—》重量级锁。注意:升级并不一定是一级一级生的,比如:直接由无锁状态升级为轻量级锁。

2.4 synchronized原理

毋庸置疑,使用synchronized关键字,可以非常快速地帮助我们解决线程安全问题,那么它到底是怎么实现的呢?

我们先来看看使用synchronized修饰的代码的字节码。

public class CustomService {

    private static int num;

    public synchronized void test1() {
        num++;
    }

    public static synchronized void test2() {
        num++;
    }

    public void test3() {
        synchronized (this) {
            num++;
        }
    }
}

通过javap -v xx.class查看对应的字节码指令:

test1():

test2():

test3():

可以得出以下结论:

1)synchronized修饰方法时,会在访问标识符(flags)中加入ACC_SYNCHRONIZED标识。

官方解释为:方法级同步是隐式执行的。当调用这些方法时,如果发现会ACC_SYNCHRONIZED标识,则会进入一个monitor,执行方法,然后退出monitor。无论方法调用正常还是发生异常,都会自动退出monitor,也就是释放锁。

2)synchronized修饰代码块时,会增加monitorentermonitorexit指令。

官方解释为:每个对象都与一个监视器monitor关联,执行monitorenter指令的线程尝试获取锁对象关联的监视器monitor的所有权,如果monitor的计数为0,则该线程获得锁,并将计数+1,此时其他线程将阻塞等待,直到该计数为0时,其他线程才有机会来获取监视器monitor的所有权。

因此,synchronized关键字的底层是通过每个对象关联的监视器monitor来实现的,每个对象关联一个监视器monitor,线程通过修改monitor的计数值来获取和释放锁。

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

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

相关文章

同步FIFO、异步FIFO详细介绍、verilog代码实现、FIFO最小深度计算、简答题

文章目录前言一、多bit数据流跨时钟域传输——FIFO1、FIFO分类2、常见参数3、与普通存储器的区别4、FIFO优缺点二、同步FIFO1、计数器法2、高位扩展法3、单端口和双端口RAM3.1 单端口RAM3.2 双端口RAM4、例化双端口RAM实现同步FIFO三、异步FIFO1、格雷码1.1 二进制和格雷码之间…

spring5(五):AOP操作

spring5&#xff08;五&#xff09;&#xff1a;AOP操作前言一、代理模式1、场景模拟2、代理模式2.1 概念2.2 静态代理2.3 动态代理二、AOP概述1、什么是 AOP?2、相关术语3、作用三、AOP底层原理1、AOP 底层使用动态代理2、AOP&#xff08;JDK 动态代理&#xff09;2.1 编写 J…

VR全景展示,VR全景平台,助理全景展示新模式

引言&#xff1a; VR全景展示是一种新型的展示方式&#xff0c;它利用虚拟现实技术和全景拍摄技术&#xff0c;使参观者可以身临其境地进入虚拟展览空间。这种展示方式不仅能够提供更加沉浸式的参观体验&#xff0c;还可以解决传统展览所面临的时间和地域限制等问题。 VR全景展…

【Java实战篇】Day7.在线教育网课平台

文章目录一、需求&#xff1a;课程审核1、需求分析2、建表与数据模型3、接口定义4、Mapper层开发5、Service层开发6、完善controller层二、需求&#xff1a;课程发布1、需求分析2、建表与数据模型3、技术方案4、接口定义5、消息处理SDK6、Mapper层开发7、Service层开发8、页面静…

unity,射手游戏

文章目录介绍一&#xff0c;制作玩家具体函数脚本PlayerCharacter三、 制作玩家控制脚本 PlayerController&#xff0c;调用上面的函数方法四、 制作子弹脚本 shell五、 给玩家挂载脚本六、 制作坦克脚本七、 给坦克添加组件八、 开始游戏&#xff0c;播放动画九、 下载介绍 3…

seata学习笔记

Seata 官网&#xff1a; https://seata.io/zh-cn/index.html 是什么&#xff1f; Seata 是一款开源的分布式事务解决方案&#xff0c;致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式&#xff0c;为用户打造一站式的分布式…

MySQL中的float类型慎用!慎用!慎用!

在前端输入一串数字后有时候展示值与输入的内容一致&#xff0c;有时候却不一致。经分析&#xff0c;原来是MySQL数据库中字该字段的类型是float&#xff0c;该字段的值超过6位有效数字后就会进行四舍五入截取&#xff0c;举例来说&#xff1a;假设float类型字段的输入值是1234…

十八、MySQL 变量、分支结构IF、CASE...WHEN详解

文章目录一、变量1.1 系统变量1.1.1 系统变量分类1.1.2 查看系统变量1.2 用户变量1.2.1 用户变量分类1.2.2 会话用户变量1.2.3 局部变量1.2.4 对比会话用户变量与局部变量二、定义条件与处理程序2.1 案例分析2.2 定义条件2.3 定义处理程序2.4 案例解决三、流程控制3.1 分支结构…

[C++]类与对象下篇

目录 类与对象下篇&#xff1a;&#xff1a; 1.再谈构造函数 2.static成员 3.友元 4.内部类 5.匿名对象 6.拷贝对象时的编译器优化 7.再次理解封装 8.求12...n(不能使用乘除法、循环、条件判断) 9.计算日期到天数的转换 10.日期差值 11.打印日期 12.累加天数 类与对象下篇&…

数据结构与算法七 堆

一 堆 1.1 堆定义 堆是计算机科学中一类特殊的数据结构的统称&#xff0c;堆通常可以被看做是一棵完全二叉树的数组对象。 堆的特性&#xff1a; 它是完全二叉树&#xff0c;除了树的最后一层结点不需要是满的&#xff0c;其它的每一层从左到右都是满的&#xff0c;如果最后…

4月最新编程排行出炉,第一名ChatGPT都在用~

作为一名合格的&#xff08;准&#xff09;程序员&#xff0c;必做的一件事是关注编程语言的热度&#xff0c;编程榜代表了编程语言的市场占比变化&#xff0c;它的变化更预示着未来的科技风向和机会&#xff01; 快跟着一起看看本月排行有何看点&#xff1a; 4月Tiobe排行榜前…

【CSS】使用 固定定位 实现顶部导航栏 ( 核心要点 | 固定定位元素居中设置 | 代码示例 )

文章目录一、核心要点分析1、顶部导航栏要点2、固定定位垂直居中设置二、代码示例一、核心要点分析 实现下图所示功能 : 上方有一个固定导航栏 , 水平居中设置 ;左右两侧各一个广告栏 , 垂直居中设置 ; 1、顶部导航栏要点 顶部导航栏要点 : 使用固定定位 , 上边偏移设置为 0 …

Linux Ubuntu虚拟机下载安装以及初始配置--VMware、Ubuntu、Xshell、Xftp

一、下载准备 Ubuntu系统下载链接&#xff08;系统本身&#xff09;&#xff1a;官网链接 VMware虚拟机下载链接&#xff08;搭载Ubuntu系统&#xff09;&#xff1a;网盘链接密码XMKD Xshell下载链接&#xff08;虚拟机远程连接&#xff09;&#xff1a;官网链接 Xftp下载…

MySQL索引数据结构入门

之前松哥写过一个 MySQL 系列&#xff0c;但是当时是基于 MySQL5.7 的&#xff0c;最近有空在看 MySQL8 的文档&#xff0c;发现和 MySQL5.7 相比还是有不少变化&#xff0c;同时 MySQL 又是小伙伴们在面试时一个非常重要的知识点&#xff0c;因此松哥打算最近再抽空和小伙伴们…

PyQt5学习笔记一、安装PyQt5和在PyCharm中配置工具

一、安装PyQt5 1. 可以在cmd窗口安装PyQt5和工具 可以在cmd窗口使用命令 pip install PyQt5 安装PyQt5&#xff0c;若指定版本使用命令 pip install PyQt5version&#xff0c;此时同时安装了PyQt5和sip。参考链接 在cmd命令窗口安装Python模块_Mr. 李大白的博客-CSDN博客htt…

potPlay——记忆播放位置、各种快捷键

potPlay——记忆播放位置、各种快捷键potPlay——各种快捷键简洁版完整版快捷键列表potPlay——记忆播放位置potPlay——各种快捷键 简洁版 Q 复位 亮度&#xff0c;对比度&#xff0c;色度复位键 W/E 调暗/调亮 R/T 对比度 Y/U 饱和度 I/O 色彩度 D 上一帧 F 下一帧 M 静音 …

Docker开启并配置远程安全访问

前言 在工作学习中&#xff0c;为了提高项目部署效率&#xff0c;一般会在Idea中直接使用Docker插件连接服务器Docker容器&#xff0c;然后将项目打包与DockerFile一起build成Docker镜像部署运行。但是不可能服务器总是跟着主机的&#xff0c;因此呢时常会面临的一个问题就是从…

【微信小程序】-- uni-app 项目--- 购物车 -- 配置 tabBar 效果(五十一)

&#x1f48c; 所属专栏&#xff1a;【微信小程序开发教程】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &…

事务的ACID特性

1. 絮絮叨叨 重温Apache ORC时&#xff0c;发现ORC支持ACID想起自己之前一度不知道ACID是哪些单词的缩写&#xff0c;更别提面试中常提到的事物隔离级别等知识了因此&#xff0c;特地学习一下数据库中事务的ACID 2. ACID 2.1 What’s transaction&#xff1f; 考虑一个真实…

42.原型对象 prototype

目录 1 面向对象与面向过程 2 原型对象 prototype 3 在内置对象中添加方法 4 constructor 属性 5 实例对象原型 __proto__ 6 原型继承 7 原型链与instanceof 7.1 原型链 7.2 instanceof 8 案例-模态框 1 面向对象与面向过程 编程思想有 面向过程 与 面向…