聊聊并发编程——多线程之synchronized

目录

一.多线程下数据不一致问题

二.锁和synchronized

2.1 并发编程三大特性

2.2引入锁概念

三.synchronized的锁实现原理

3.1 monitorenter和monitorexit

3.2synchronized 锁的升级

3.2.1偏向锁的获取和撤销

3.2.2轻量级锁的加锁和解锁

自适应自旋锁

轻量级锁的解锁

3.2.3重量级锁—线程阻塞

3.2.4锁的优缺点对比

3.3CAS实现原子性


一.多线程下数据不一致问题

我们知道,在并发编程中,多个线程同时访问共享资源时可能导致数据不一致、死锁、性能问题等严重后果。

像下面这个例子,i的值最后是多少呢?

简单的demo实验下:

public class Demo {private static int count = 0;public static void inc() {try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}count++;}
​public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 100; i++) {new Thread(() -> Demo.inc()).start();}Thread.sleep(3000);System.out.println("运行结果" + count);}
}

多次的运行结果是不相同的,并不是预期的100:

针对数据不一致的问题,我们先了解下java内存模型(Java Memory Model, JMM),它是一种抽象的模型,被定义出来屏蔽各种硬件和操作系统的内存访问差异。JMM定义了线程与主内存之间的抽象关系:线程之间的共享变量存储在主内存中,而每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。

结合JMM不难理解,线程t1和t2通过操作本地内存中的副本进行刷新存储在主内存上的共享变量对象,但它们都没有在进行修改操作后立即告知其它线程。针对多线程并发访问共享资源导致数据不一致的问题,java给出了类似Synchronized和volatile的解决方案。

二.锁和synchronized

2.1 并发编程三大特性

并发编程中有三大核心特性,分别是原子性(Atomicity)、可见性(Visibility)、有序性(Ordering),通常被简称为 AVO 特性。这些特性对于多线程编程非常重要,因为它们确保了多线程环境下的正确性和可靠性。

  1. 原子性(Atomicity):

    • 原子性指的是一个操作是不可分割的单元,要么全部执行成功,要么全部不执行,不会被中断。原子操作可以看作是线程安全的,多个线程可以同时执行这个操作,而不会破坏操作的完整性。

    • 原子性保证了操作的完整性,避免了竞态条件,通常通过锁、原子类、事务等机制来实现。

  2. 可见性(Visibility):

    • 可见性指的是一个线程对共享变量的修改能够被其他线程立即感知到,即修改后的值在主内存中对其他线程是可见的。在多线程环境下,如果不采取适当的同步措施,共享变量的修改可能对其他线程不可见,导致意外的行为和错误。

    • 可以使用volatile关键字来确保变量的可见性,或者使用锁机制(如synchronized关键字)来同步访问共享变量。

  3. 有序性(Ordering):

    • 有序性指的是程序的执行顺序与代码的编写顺序一致,即代码按照预期的顺序执行,不会出现乱序或重排序的情况。

    • 在现代计算机架构中,为了提高性能,编译器和处理器可能会对指令进行重排序,但这种重排序在单线程环境下不会引发问题。然而,在多线程环境下,如果不正确地控制重排序,可能会导致不一致的结果。

    • 可以使用同步机制来确保有序性,例如在进入和退出锁的范围内,编译器和处理器会执行必要的指令重排序来维护有序性。

2.2引入锁概念

在可见性的理解上,java引入了锁的概念,用于防止多个线程同时访问或修改共享资源,以确保线程安全性。

而加锁的方式是使用synchronized关键字。

public class Demo {private static int count = 0;// 使用Synchronized加锁public static synchronized void inc() {try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}count++;}
​public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 100; i++) {new Thread(() -> Demo.inc()).start();}Thread.sleep(3000);System.out.println("运行结果" + count);}
}

synchronized 关键字在 Java 中主要用于实现互斥锁,它主要保证了并发编程中的原子性和可见性这两个特性。

  1. 原子性(Atomicity):

    synchronized 保证了被 synchronized 修饰的代码块或方法在同一时间只能被一个线程执行,这确保了其中的操作是原子的,即不会被中断或同时被其他线程访问。这有助于防止竞态条件和确保操作的完整性。

  2. 可见性(Visibility):

    当一个线程进入 synchronized 块或方法时,它会获取锁,并在释放锁时将修改的数据从线程的工作内存同步到主内存,这就确保了其他线程可以立即看到最新的数据。这满足了可见性要求,确保了共享数据的变更对其他线程是可见的。

synchronized 关键字在 Java 中有三种主要的用法,用于实现不同类型的同步:

  1. 实例方法同步:

    使用 synchronized 关键字修饰普通的实例方法,这会将锁定范围限制在该方法所属对象实例上,即同一对象的不同线程在同时访问这个方法时会互斥执行。这种方式适用于对实例变量的同步访问。

    public synchronized void instanceMethod() {// 这里的代码是线程安全的
    }
  2. 静态方法同步:

    使用 synchronized 关键字修饰静态方法,这会将锁定范围限制在类的 Class 对象上,即同一类的不同对象的不同线程在同时访问这个静态方法时会互斥执行。这种方式适用于对静态变量的同步访问。

    public static synchronized void staticMethod() {// 这里的代码是线程安全的
    }
  3. 同步代码块:

    使用 synchronized 关键字修饰代码块,可以精确地指定需要同步的对象。这允许你在方法内的特定部分实现同步,而不是整个方法。你需要指定一个对象作为锁,当多个线程尝试进入同步代码块时,它们必须获取相同的锁才能执行。

    public void someMethod() {// 一些非同步代码synchronized (lockObject) {// 这里的代码是线程安全的,锁定的是 lockObject 对象}// 更多非同步代码
    }

    设计模式中

    [单例模式的双重检查锁定]  https://blog.csdn.net/Elaine2391/article/details/132675080 

    其实就是一个简单的Synchronized和volatile的应用。

三.synchronized的锁实现原理

3.1 monitorenter和monitorexit

我们使用synchronized的时候没有手动的lock和unlock,那么synchronized是怎么加锁的呢?其实这里JVM帮我们处理了。反编译⼀段synchronized修饰代码块代码来看看:

找到上面我们的Demo类文件路径,javac Demo.java后生成class文件, javap -v Demo.class ,可以看到相应的字节码指令。synchronized修饰静态方法时,JVM采用monitorenter和monitorexit两个指令来实现同步,monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指向同步代码块的结束位置。

而且synchronized修饰同步方法时,JVM采用ACC_SYNCHRONIZED标记符实现同步,这个标识指明了该方法是一个同步方法。

monitorenter、monitorexit或者ACC_SYNCHRONIZED都是基于Monitor实现的。Monitor是什么呢?

所谓的Monitor其实是⼀种同步⼯具,也可以说是⼀种同步机制。在Java虚拟机(HotSpot)中,Monitor是由 ObjectMonitor实现的,可以叫做内部锁,或者Monitor锁。线程在获取锁的时候,实际上就是获得一个监视器对象(monitor),monitor可以认为是一个同步对象,所有的Java对象是天生携带monitor。多个线程访问同步代码块时,相当于去争抢对象监视器修改对象中的锁标识,ObjectMonitor这个对象和线程争抢锁的逻辑有密切的关系。

3.2synchronized 锁的升级

在JVM的自动内存管理中分析markword时,提到了偏向锁、轻量级锁、重量级锁和无锁状态。分析前我们先来思考一个问题:使用锁能够实现数据的安全性,但是会带来性能的下降。不使用锁能提高性能又不能保证线程安全性。怎么办呢?

其实hotspot的作者经过调查发现,假设加锁的同步块分为下面三种情况,而大部分情况下,是处于第一种。

  1. 只有线程A进入临界区。(偏向锁)

  2. 线程A和线程B交替进入临界区。(轻量级锁)

  3. 线程A、线程B、线程C同时进入临界区。(重量级锁)

而这也是JDK1.6之后synchronized做出的优化,为了减少获得锁和释放锁带来的性能开销,引入了偏向锁、轻量级锁的概念。所以synchronized中,锁存在四种状态,分别是无锁、偏向锁、轻量级锁、重量级锁;锁的状态根据竞争激烈的程度从低到高不断升级。

针对共享资源对象加锁的操作,其实真正操作的是对象头中的markword。

img

3.2.1偏向锁的获取和撤销
  • 首先获取锁对象的Markword,判断是否处于可偏向状态。(偏向锁位biased_lock=0且ThreadId为空,表示未偏向任何线程)

  • 如果是可偏向状态,则通过CAS操作,把当前线程的ID写入到Markword

    a)如果CAS成功,那么就表示线程获取了锁对象的偏向锁,锁状态从无锁升级为偏向锁。

    b)如果CAS失败,说明其他线程已获得了偏向锁。这种情况说明当前存在锁竞争,需要暂停已获得偏向锁的线程,撤销偏向锁,升级为轻量级锁(这个操作在全局安全点执行,就是没有线程在执行字节码的时候)

  • 如果是已偏向状态,需要检查Markword中存储的ThreadId是否等于当前线程的ThreadId

    a)如果相等,不需要再次获得锁,可直接执行同步代码块。这就避免了锁的竞争和线程上下文切换。

    b)如果不相等,说明当前锁偏向于其他线程,需要撤销锁并升级为轻量级锁

对于原持有偏向锁的线程进行撤销时,原获得偏向的线程有两种情况:

a)原获得偏向锁的线程已经退出了临界区,就是执行完了代码块,这时候就会把对象头设置为无锁状态,并且争抢锁的进程可以基于CAS重新设置对象头偏向当前线程。

b)原获得偏向锁的线程还在执行同步块,这个时候就会将原获得偏向锁的线程的偏向锁升级为轻量级锁。

但是根据我们实际情况,绝大部分时候一定会存在2个以上的线程竞争,那么开启偏向锁反而提升了获取锁的资源消耗,可以通过jvm参数UserBiasedLocking来设置开启或关闭偏向锁。流程图分析如下:

3.2.2轻量级锁的加锁和解锁
  • 首先,jvm会判断是否是重量级锁,如果不是,会在当前线程栈帧中划出一块空间,作为该锁的锁记录,并且将锁对象Markword复制到该锁记录中。

  • 复制成功后,jvm使用CAS操作将对象头Markword更新为指向锁记录的指针,并且将锁记录里的own指针指向对象头的Markword。

  • 更新对象头Markword成功的情况,当前线程持有对象锁,并且对象Markword锁标志设为‘00’,即表示此对象处于轻量级锁状态。

  • 更新对象头Markword失败的情况,jvm先检查对象MarkWord是否指向当前线程栈帧中的锁记录,如果是就表示锁重入。如果不是就表示锁对象被其他线程抢占,进行自旋等待(默认10次),等待次数达到阈值仍未获取到锁,膨胀为重量级锁。

自适应自旋锁

自旋锁就是锁在原地循环执行一个啥都没有的for循环操作,是会消耗cpu的。JDK1.6后引入了自适应自旋锁,自适应意味着着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。

如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自

旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。

如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

轻量级锁的解锁

就是获得锁的逆向逻辑,通过CAS操作把栈帧中的LockRecord替换回锁对象的Markword中,如果成功表示没有竞争。如果失败,表示当前锁存在竞争,那么轻量级锁就会膨胀为重量级锁。流程图如下:

3.2.3重量级锁—线程阻塞

轻量级锁膨胀到重量级锁后,线程只能被挂起阻塞来等待被唤醒了。阻塞是重量级锁的标志。重量级锁加锁的基本流程:

举例说明下:

定义线程A传入锁对象lock,包含一个简单的wait操作。

public class ThreadA extends Thread{private Object lock;public  ThreadA(Object lock){this.lock = lock;}@Overridepublic void run() {synchronized (lock) {System.out.println("ThreadA start");try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("ThreadA end");}}
}

定义线程B传入锁对象,包含一个notify操作。

public class ThreadB extends Thread{private Object lock;public ThreadB(Object lock){this.lock = lock;}@Overridepublic void run() {synchronized (lock) {System.out.println("ThreadB start");lock.notify();System.out.println("ThreadB end");}}
}

创建A线程启动,B线程启动,执行结果:

public class WaitNotify {public static void main(String[] args) {Object lock = new Object();ThreadA threadA = new ThreadA(lock);threadA.start();
​ThreadB threadB = new ThreadB(lock);threadB.start();}
}

3.2.4锁的优缺点对比

3.3CAS实现原子性
  • CAS叫做CompareAndSwap,⽐较并交换,主要是通过处理器的指令来保证操作的原⼦性的。

  • CAS 指令包含 3 个参数:共享变量的内存地址 A、预期的值 B 和共享变量的新值 C。

  • 只有当内存中地址 A 处的值等于 B 时,才能将内存中地址 A 处的值更新为新值 C。作为⼀条 CPU 指令,CAS 指令 本身是能够保证原⼦性的

CAS操作三大问题

  • ABA问题,在比较期间,发生了A-B-A,CAS感知不到过程,解决方案:加版本号。1A-2B-3A.

  • 循环时间长开销大

  • 只能保证一个共享变量的原子操作

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

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

相关文章

Apache DolphinScheduler 在奇富科技的首个调度异地部署实践

奇富科技&#xff08;原360数科&#xff09;是人工智能驱动的信贷科技服务平台&#xff0c;致力于凭借智能服务、AI研究及应用、安全科技&#xff0c;赋能金融机构提质增效&#xff0c;助推普惠金融高质量发展&#xff0c;让更多人享受到安全便捷的金融科技服务。作为国内领先的…

从裸机开始安装操作系统

目录 一、预置知识 电脑裸机 win10版本 官方镜像 V.S. 正版系统 二、下载微软官方原版系统镜像 三、使用微PE系统维护U盘 四、安装操作系统 五、总结 一、预置知识 电脑裸机 ●只有硬件部分&#xff0c;还未安装任何软件系统的电脑叫做裸机。 ●主板、硬盘、显卡等必…

start()方法源码分析

当我们创建好一个线程之后&#xff0c;可以调用.start()方法进行启动&#xff0c;start()方法的内部其实是调用本地的start0()方法&#xff0c; 其实Thread.java这个类中的方法在底层的Thread.c文件中都是一一对应的&#xff0c;在Thread.c中start0方法的底层调用了jvm.cpp文件…

static const char* 和const char*有报错,发生访问冲突

const char *srcfilere aByteArray.data(); 当重复某一操作时&#xff1a;有报错&#xff0c;发生访问冲突 const char *srcfilere aByteArray.data(); 即可解决访问冲突问题。 困扰了至少一天吧 应该是static的问题&#xff0c;吃了基础不好的亏。

AUTOSAR中的Crypto Stack(二)--CSM数据类型解析

在上一节,简单梳理了加密栈的基本要求。其中最关键最核心的还是用户如何使用HSM这个黑盒子,这就必须要对Crypto Service Manager要有很清晰的认识。 那么首先我们还是围绕概述里提到的job类型进行分析。 1. Crypto_JobType 上图, 在AUTOSAR的架构里,所有的密码操作…

【OLSR路由协议】链路状态路由(OLSR)协议中选择多点中继节点算法研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

Grafana设置默认主页

点击【设置/管理】-> 【默认首选项 Preferences】-> 【主页仪表盘】 在下拉中选择一个页面作为主页即可

SPA项目之登录注册--请求问题(POSTGET)以及跨域问题

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于VueElementUI的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.ElementUI是什么 &#x1f4a1;…

SpringSecurity 入门

文章目录 Spring Security概念快速入门案例环境准备Spring配置文件SpringMVC配置文件log4j配置文件web.xmlTomcat插件 整合SpringSecurity 认证操作自定义登录页面关闭CSRF拦截数据库认证加密认证状态记住我授权注解使用标签使用 Spring Security概念 Spring Security是Spring…

docker 安装 nessus新版、awvs15-简单更快捷

一、docker 安装 nessus 参考项目地址&#xff1a; https://github.com/elliot-bia/nessus 介绍&#xff1a;几行代码即可一键安装更新 nessus -推荐 安装好 docker后执行以下命令 #拉取镜像创建容器 docker run -itd --nameramisec_nessus -p 8834:8834 ramisec/nessus …

C#中的(++)和(--)运算符

目录 背景: 的前加 效果展示:​ 的后加 效果展示 :​ 总结: 背景: 自增和自减运算符存在于C/C/C#/Java等高级语言中&#xff0c;它的作用是在运算结束前(前置自增自减运算符 )或后(后置自增自减运算符 )将 变量的值加(或减)1。 在C#中&#xff0c;和--是自增和自减运…

【趣味JavaScript】5年前端开发都没有搞懂toString和valueOf这两个方法!

&#x1f680; 个人主页 极客小俊 ✍&#x1f3fb; 作者简介&#xff1a;web开发者、设计师、技术分享博主 &#x1f40b; 希望大家多多支持一下, 我们一起进步&#xff01;&#x1f604; &#x1f3c5; 如果文章对你有帮助的话&#xff0c;欢迎评论 &#x1f4ac;点赞&#x1…

UE5 虚幻引擎 如何使用构造脚本(Construction Script)? 构造脚本的奥秘!

目录 1 构造脚本&#xff08;Construction Script&#xff09;1.1 介绍1.2 案例1&#xff1a;利用样条组件程序化生成树木1.2 案例2&#xff1a;利用样条组件和样条网格体组件程序化生成道路 1 构造脚本&#xff08;Construction Script&#xff09; 1.1 介绍 问题&#xff1a…

【考研数学】概率论与数理统计 —— 第三章 | 二维随机变量及其分布(2,常见的二维随机变量及二维变量的条件分布和独立性)

文章目录 引言四、常见的二维随机变量4.1 二维均匀分布4.2 二维正态分布 五、二维随机变量的条件分布5.1 二维离散型随机变量的条件分布律5.2 二维连续型随机变量的条件分布 六、随机变量的独立性6.1 基本概念6.2 随机变量独立的等价条件 写在最后 引言 有了上文关于二维随机变…

【Vue.js】使用ElementUI搭建动态树数据表格与分页

一&#xff0c;动态树 本文章为上一篇文章拓展内容》》实现首页导航及左侧菜单 将左侧菜单结构更换为下面代码&#xff1a; 菜单结构&#xff1a; <el-menu><el-submenu index"" key""><template slot"title"><i class…

1526. 形成目标数组的子数组最少增加次数;2008. 出租车的最大盈利;1589. 所有排列中的最大和

1526. 形成目标数组的子数组最少增加次数 核心思想&#xff1a;差分数组。对于一个数组a,要想从全为0的数组增加1变为它&#xff0c;等价于从a减少1变为全0的数组。然后a有一个差分数组d&#xff0c;对于a区间的[L,R]减少1操作等价于对d[L]-1,然后d[R1]1。你想让a变为全0&…

灰色预测GM(1,1)

目录 一、灰色预测模型简介 二、GM(1,1)灰色模型 1、GM(1,1)模型预测方法 &#xff08;1&#xff09;原始数据&#xff08;参考列&#xff09; &#xff08;2&#xff09;累加生成序列&#xff08;Acumulated Generating Operator,1-AGO&#xff09; &#xff08;3&#xff…

PHY6252—超高性价比蓝牙/2.4G芯片

PHY6252是用于蓝牙5.2应用的芯片&#xff08;SOC&#xff09;系统。它具有高性能的低功率32位处理器&#xff0c;具有64K保留SRAM&#xff0c;512/256KB flash&#xff0c;96KB ROM&#xff0c;256bit Efuse和超低功率&#xff0c;高性能&#xff0c;多模式广播。此外&#xff…

03 MIT线性代数-矩阵乘法和逆矩阵Multiplication inverse matrices

1. 矩阵乘法 Matrix multiplication 我们通过四种方法讨论如何使矩阵A与B相乘得到矩阵C。其中A为mxn&#xff08;m行n列&#xff09;矩阵&#xff0c;而B为nxp矩阵&#xff0c;则C为mxp矩阵&#xff0c;记cij为矩阵C中第i行第j列的元素 1.1 Regular way 矩阵乘法的标准计算方…

分享从零开始学习网络设备配置--任务4.1 IPv6地址的基本配置

任务描述 某公司构建了互联互通的办公网&#xff0c;需要不断扩大网络规模。网络管理员小赵决定采用IPv6的地址&#xff0c;满足公司网络规模的未来发展。 由于IPv4地址耗尽及IPv4地址区域分配不均衡&#xff0c;成为运营商必须面临的一个问题。另外随着互联网的商业化&#…