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

news2024/11/22 15:50:38

目录

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

二.锁和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;
    }
    @Override
    public 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;
    }
    @Override
    public 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.coloradmin.cn/o/1041376.html

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

相关文章

位运算练习题(Java)

package KeepCoding.algorithm; //位运算知识点 /* 1. 0 ^ x x x ^ x 1 * 2. 位运算满足结合律和交换律&#xff0c;即运算顺序无关 */ //位运算练习题 //1.整数数组中有一个出现次数为奇数的整数&#xff0c;其余整数的出现次数均为偶数个&#xff0c;请找出数组中这位…

oracle

title: “Oracle” createTime: 2021-12-13T16:35:4108:00 updateTime: 2021-12-13T16:35:4108:00 draft: false author: “name” tags: [“oracle”] categories: [“db”] description: “测试的” 时间字段分析 timestamp 精确到秒后面6位 createTime: 2021-12-13T16:35:…

微积分学习笔记(2):用Go语言画函数图像

使用的Go库 gonum.org/v1/plotimage/color 待绘图函数 直接使用三角函 s i n sin sin: func f(x float64) float64 {return math.Sin(x) }绘图过程 1 创建一个绘图 p : plot.New()assert.NotNil(t, p)p.Title.Text "Function Image"p.X.Label.Text "X&qu…

基于微信小程序的中医知识库系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言用户微信小程序端的主要功能有&#xff1a;管理员的主要功能有&#xff1a;具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌…

crypto:Url编码

题目 下载题目所给的压缩包后解压&#xff0c;打开文件可得 由题目可知为url编码&#xff0c;所以使用解码工具解码即可得到flag

【深度学习实验】卷积神经网络(二):实现简单的二维卷积神经网络

目录 一、实验介绍 二、实验环境 1. 配置虚拟环境 2. 库版本介绍 三、实验内容 0. 导入必要的工具包 1. 二维互相关运算&#xff08;corr2d&#xff09; 2. 二维卷积层类&#xff08;Conv2D&#xff09; a. __init__&#xff08;初始化&#xff09; b. forward(前向传…

React基础教程(四):组件理解

1、函数式组件 实现代码 <script type"text/babel"> /*此处一定要写babel*/ // 1、创建函数式组件 function MyComponent() {console.log(this); // 此处的this是undefined&#xff0c;因为babel编译后开启了严格模式return <h2>我是用函数定义的组件&am…

unable to access xxxx: Failed to connect to xxxx

问题&#xff1a; 1、GitLab仓库加上双重验证后&#xff0c;设置GIt得 Manage Remotes时报错 unable to access xxxx: Failed to connect to xxxx SSL certificate problem:self signed certificate 解决 1、返回前面得操作步骤检查了一遍 没有问题 2、最后尝试一些方法解…

SPA项目之主页面--Mock.js以及组件通信(总线)的运用

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于VueElementUI的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.Mock.js是什么 二.为什么要使用…

跨域的解决方案

文章目录 概念一、什么是跨域问题二、为什么会发生跨域问题三、跨域解决方案1、JSONP2、添加响应头3、Spring注解CrossOrigin4、配置文件&#xff08;常用&#xff09;5、nginx跨域 概念 一、什么是跨域问题 前端调用的后端接口不属于同一个域&#xff08;域名或端口不同&…

Docker 容器监控之CAdvisor+InfluxDB+Granfana

是什么 一句话&#xff1a;CAdvisor监控收集InfluxDB存储数据Granfana展示图表 CAdvisor InfluxDB Granfana 总结 容器编排CIG CIG CAdvisorInfluxDBGranfana 1、新建目录 2、新建docker-compose.yml文件 version: 3.1volumes:grafana_data: {}services:influxdb:image: t…

1*1的卷积核如何实现降维/升维?

在众多网络中&#xff0c;1*1的卷积核被引入用来实现输入数据通道数的改变。 举例说明&#xff0c;如果输入数据格式为X*Y*6&#xff0c;X*Y为数据矩阵&#xff0c;6为通道数&#xff0c;如果希望输出数据格式为X*Y*5&#xff0c;使用5个1*1*6的卷积核即可。 其转换过程类似于…

C/C++程序,从命令行传入参数

C/C中如何让程序接受并处理命令行参数_c 命令行接收--version_饼干叔叔海洋的博客-CSDN博客 #include <stdio.h> #include <stdlib.h>//argc&#xff1a;argument count //argv: arguments vector,参数向量。 //33 777 103 int main(int argc, char **argv){ //…

配置OSPFv3引入外部路由及路由过滤 华为实验

1.1 实验介绍 1.1.1 关于本实验 在大型园区网络中&#xff0c;往往使用不同的路由协议进行组网&#xff0c;实现全网的网络互通。不同的协议间通信&#xff0c;除了路由协议本身&#xff0c;还需要引入外部路由及路由信息过滤等技术。 本章内容主要介绍OSPFv3路由过滤及引入外…

QT配置MySQL数据库 ninja: build stopped: subcommand failed

QT配置MySQL数据库 我当前的软件版本&#xff1a;QT Creator 10.0.2 (community)&#xff0c;MingW 6.4.3 (QT6)&#xff0c;MySQL 8.0。 MySQL不配置支持的数据库有QList("QSQLITE", "QODBC", "QPSQL")&#xff0c;这个时候是不支持MYSQL数据…

Rust之自动化测试(二):控制测试如何运行

开发环境 Windows 10Rust 1.72.1 VS Code 1.82.2 项目工程 这里继续沿用上次工程rust-demo 控制测试如何运行 正如cargo run编译您的代码&#xff0c;然后运行生成的二进制文件一样&#xff0c;cargo test在测试模式下编译您的代码&#xff0c;然后运行生成的测试二进制文件…

【计算机网络笔记五】应用层(二)HTTP报文

HTTP 报文格式 HTTP 协议的请求报文和响应报文的结构基本相同&#xff0c;由四部分组成&#xff1a; ① 起始行&#xff08;start line&#xff09;&#xff1a;描述请求或响应的基本信息&#xff1b;② 头部字段集合&#xff08;header&#xff09;&#xff1a;使用 key-valu…

VR全景智慧文旅解决方案,助力文旅产业转型升级

引言&#xff1a; 随着科技的不断发展&#xff0c;虚拟现实&#xff08;VR&#xff09;技术正逐渐展露其影响力&#xff0c;改变着旅游业。VR全景智慧文旅解决方案也应运而生&#xff0c;将传统旅游的体验形式从线下扩展到了线上&#xff0c;带来了不一般的文旅体验。 一.VR全…