深入理解Java中的synchronized

news2024/11/16 21:57:31

文章目录

  • 前言
  • 正文
    • 一、多线程操作同一数据时的问题
    • 二、问题分析
    • 三、synchronized 解决问题
    • 四、synchronized 是怎么解决问题的
    • 五、Java1.6时的优化
      • 5.1 自旋锁
      • 5.2 自适应锁
      • 5.3 锁消除
      • 5.4 锁粗化
      • 5.5 偏向锁(单线程高效场景)
      • 5.2 轻量级锁(多线程交替时)

前言

Java 中有这样一个关键字,它挑起了Java锁的半边天。甚至于在一些Java面试中,也会经常被提出来讨论。它就是 synchronized,今天我们就一起来看看它在Java锁中起到了什么样的作用把!

正文

一、多线程操作同一数据时的问题

首先来看一个简单的例子,多线程场景对一个对象中的某个属性进行操作。定义一个Demo类,并且增加属性 num,如下类图:
在这里插入图片描述
然后开启多个线程对Demo中的 num属性进行操作(增加或减少)。这里开启100个线程,对同一个Demo实例中的num进行自增,具体代码:

import java.util.concurrent.TimeUnit;

public class Demo {

    private Integer num = 0;

    public static void main(String[] args) throws InterruptedException {
        Demo demo = new Demo();
        // 开启线程执行任务
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                demo.num++;
            }).start();
        }

		// 等待线程执行完毕
        TimeUnit.SECONDS.sleep(1);
        // 输出结果
        System.out.println(demo.num);
    }
}

分析以上代码,我们可以看到,正常情况下num值在最终会自增到100,但是实际情况却是到不了100。
我在这里执行了多次,最终打印的结果有9698等。甚至于你的电脑如果跑的更快的话,这个值还能更小(距离100会更远)。

这里的原因还是比较好理解的,主要是多线程操作了同一对象中的同一属性,发生了数据不同步的问题。
这里调整一下线程里的执行代码:

import java.util.concurrent.TimeUnit;

public class Demo {

    private Integer num = 0;

    public static void main(String[] args) throws InterruptedException {
        Demo demo = new Demo();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                demo.num++;
                System.out.println(Thread.currentThread().getName() + "执行num计算,值:" + demo.num);
            }, "thread-" + i).start();
        }

        TimeUnit.SECONDS.sleep(1);
        System.out.println(demo.num);
    }
}

现在我们创建20个线程对数据进行计算,并且打印出线程名和计算时的num的值。

thread-19执行num计算,值:16
thread-9执行num计算,值:4
thread-6执行num计算,值:12
thread-7执行num计算,值:6
thread-14执行num计算,值:10
thread-18执行num计算,值:17
thread-15执行num计算,值:15
thread-17执行num计算,值:19
thread-12执行num计算,值:14
thread-11执行num计算,值:11
thread-4执行num计算,值:7
thread-2执行num计算,值:2
thread-8执行num计算,值:5
thread-1执行num计算,值:13
thread-16执行num计算,值:18
thread-13执行num计算,值:9
thread-0执行num计算,值:1
thread-10执行num计算,值:3
thread-3执行num计算,值:8
thread-5执行num计算,值:7
19

可以观察到thread-4 和 thread-5的计算结果是相同的,也就是说他们哥俩在获取num的原值的时候,拿到了相同的原值,因此导致了现在的情况。

二、问题分析

首先我们依据JMM模型对以上数据变化进行分析:
在这里插入图片描述
线程会从主内存拿到共享变量的值,然后在自己的工作内存保存一份副本,最终运行的结果也是使用的这个副本。

多线程场景下,这样就会出现问题。

  • 场景1:线程1拿到共享变量,并执行一些计算,随后将共享变量的值写回给主内存,在这之后,线程2才开始跑,线程2从主内存拿到了线程1更新后的结果,然后开始自己的计算。
  • 场景2:线程1拿到共享变量,还没开始计算(或者没执行到写回数据给主内存的时候),线程2也开始跑,线程2从主内存拿到数据(没改过的数据),也开始进行自己的计算,最终无论他们谁先写回数据,最终的结果也是有问题的。

场景2就是我们现在遇到的情况了。

三、synchronized 解决问题

synchronized 关键字的重要作用:一个变量在同一时刻只能被一个线程对其进行lock操作,串行操作。
在原有代码中加入同步代码块:

import java.util.concurrent.TimeUnit;

public class Demo {

    private Integer num = 0;

    public static void main(String[] args) throws InterruptedException {
        Demo demo = new Demo();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                synchronized (demo){
                    demo.num++;
                    System.out.println(Thread.currentThread().getName() + "执行num计算,值:" + demo.num);
                }
            }, "thread-" + i).start();
        }

        TimeUnit.SECONDS.sleep(1);
        System.out.println(demo.num);
    }
}

运行结果发生了变化,每次执行都是相同的结果:

thread-0执行num计算,值:1
thread-11执行num计算,值:2
thread-7执行num计算,值:3
thread-2执行num计算,值:4
thread-5执行num计算,值:5
thread-4执行num计算,值:6
thread-6执行num计算,值:7
thread-8执行num计算,值:8
thread-1执行num计算,值:9
thread-10执行num计算,值:10
thread-12执行num计算,值:11
thread-13执行num计算,值:12
thread-14执行num计算,值:13
thread-3执行num计算,值:14
thread-15执行num计算,值:15
thread-19执行num计算,值:16
thread-17执行num计算,值:17
thread-18执行num计算,值:18
thread-16执行num计算,值:19
thread-9执行num计算,值:20
20

四、synchronized 是怎么解决问题的

我们使用 javap 命令查看一下编译后的内容,命令及参数如下:javap -c -v -p Demo.class。在命令行打出来的结果是包含如下内容:

private static void lambda$main$0(Demo);
    descriptor: (LDemo;)V
    flags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=3, locals=5, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_0
         5: getfield      #3                  // Field num:Ljava/lang/Integer;
         8: astore_2
         9: aload_0
        10: aload_0
        11: getfield      #3                  // Field num:Ljava/lang/Integer;
        14: invokevirtual #15                 // Method java/lang/Integer.intValue:()I
        17: iconst_1
        18: iadd
        19: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        22: dup_x1
        23: putfield      #3                  // Field num:Ljava/lang/Integer;
        26: astore_3
        27: aload_2
        28: pop
        29: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
        32: invokestatic  #16                 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
        35: invokevirtual #17                 // Method java/lang/Thread.getName:()Ljava/lang/String;
        38: aload_0
        39: getfield      #3                  // Field num:Ljava/lang/Integer;
        42: invokedynamic #18,  0             // InvokeDynamic #2:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/Integer;)Ljava/lang/String;
        47: invokevirtual #19                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        50: aload_1
        51: monitorexit
        52: goto          62
        55: astore        4
        57: aload_1
        58: monitorexit
        59: aload         4
        61: athrow
        62: return
      Exception table:
         from    to  target type
             4    52    55   any
            55    59    55   any
      LineNumberTable:
        line 11: 0
        line 12: 4
        line 13: 29
        line 14: 50
        line 15: 62
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      63     0  demo   LDemo;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 55
          locals = [ class Demo, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 6
}

其中很关键的两个指令是 monitorenter 和 monitorexit 。所有java对象都有一个monitor监视器对象存储该对象的对象头中,我这里的Demo对象也是如此。

特别注意的是,synchronized 定义为同步方法时,是使用ACC_SYNCHRONIZED进行标识,然后隐式使用了监视器指令。

monitorenter 和 monitorexit 分别表示获取和释放 monitor ,如果使用monitorenter 进入时monitor为0,表示获取到了锁,也就是当前线程可以持有monitor后续的代码,并且将monitor 加1;如果当前已经加过1,就再加1依次类推,这也是“可重入锁”的一个体现。

如果其他线程进来时,发现monitor不是0,那就得阻塞。
同样的道理,monitorexit 时会减1,直到最终减为0,表示释放了锁,其他线程才能去争抢执行。

五、Java1.6时的优化

那如果我在共享资源没有被多线程竞争时,仍然使用了同步代码块,岂不是加锁,影响了效率呢?还有就是多线程竞争时还有没有别的效率优化措施。

这一点Java开发人员已经想到了,在Jdk1.6版本时对synchronized进行了优化,提出了偏向锁,轻量级锁,重量级锁,锁升级和锁降级等概念。

这里有必要提几个概念!以下内容划重点。

5.1 自旋锁

由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线程,用户态和内核态的来回上下文切换严重影响性能。

自旋的概念就是让线程执行一个忙循环,可以理解为就是啥也不干,防止从用户态转入内核态,自旋锁可以通过设置-XX:+UseSpining来开启,自旋的默认次数是10次,可以使用-XX:PreBlockSpin设置。

5.2 自适应锁

自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。

5.3 锁消除

锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除。

5.4 锁粗化

锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。

5.5 偏向锁(单线程高效场景)

JVM利用CAS在对象头上设置线程ID表示这个对象偏向于当前线程。

也就是说,这个锁会偏向于上一个获取过它的线程。
应用场景适合单线程的情况。减少了获取锁的开销。主要优点在于,在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。

当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。可以用过设置-XX:+UseBiasedLocking开启偏向锁

在这里插入图片描述

5.2 轻量级锁(多线程交替时)

在我们开发程序过程中,多线程交替执行的场景无疑是最多的。在概念上,前面我们提到的 monitor 实现的锁叫做重量级锁。

轻量级锁是在多线程交替或者偏向锁撤销(比如偏向锁的那个线程被暂停)时,会升级为轻量级锁。主要目的是为了减少重量级锁引起的性能消耗。

对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁。

以下流程图简要说明了锁升级的各个阶段。

在这里插入图片描述

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

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

相关文章

MySQL数据表查询

&#x1f607;作者介绍&#xff1a;一个有梦想、有理想、有目标的&#xff0c;且渴望能够学有所成的追梦人。 &#x1f386;学习格言&#xff1a;不读书的人,思想就会停止。——狄德罗 ⛪️个人主页&#xff1a;进入博主主页 &#x1f5fc;专栏系列&#xff1a;进入MySQL知识专…

IO、存储、文件系统的简单介绍

目录 一.什么是IO 第一类:存储器IO 第二类:设备IO 二.存储 三:文件系统 总结 一.什么是IO I(input):放入数据 O(output):取出数据 所以我们平时说的IO,实际上就是放入数据和存储数据的意思 在这里,我们一般将IO又分为两大类 第一类:存储器IO 这类IO主要针对的是计算机中…

2023年最新同步网盘排行榜,了解哪些平台适合您的文件同步需求!

在数码领域&#xff0c;同步盘是一个极其受欢迎的工具&#xff0c;它可以帮助人们在不同设备之间共享文件。作为同步盘用户&#xff0c;我们关心的一个很重要的问题就是&#xff0c;在同步盘市场上&#xff0c;哪些同步盘是最好的&#xff1f; 今天我们综合了不同的产品测评网站…

Vulnhub: Corrosion:2靶机

kali&#xff1a;192.168.111.111 靶机&#xff1a;192.168.111.131 信息收集 端口扫描 nmap -A -sC -v -sV -T5 -p- --scripthttp-enum 192.168.111.131 通过nmap脚本枚举出8080端口存在backup.zip文件&#xff0c;下载后解压发现需要密码&#xff0c;利用john爆破压缩包密…

【SSM项目整合流程】

目录 一.用Maven创建一个project项目 1.1新建一个项目&#xff0c;选择Maven然后点击下一步 1.2设置项目名称和AGV后点击完成 1.3在pom.xml文件中导入依赖和配置打包方式 二.添加web工程 2.1在Project Structure中型添加一个web工程 2.2配置web.xml 三.创建SpringMVC的…

2.设置Salesforce开发环境

文章目录 前言1. 关于Salesforce DX环境2. 配置Visual Studio Code2.1 安装CLI2.2 设置Visual Studio Code 3. 创建一个Hello World Lightning web component 来检证开发环境3.1 创建一个Salesforce DX project3.2 将deploy好的组件加到lightning App中 前言 此处解释关于本文…

数据结构——C语言实现常见排序(插入排序、希尔排序、选择排序、堆排序、冒泡排序)

引言&#xff1a; 现在是北京时间2023年6月23日13点19分&#xff0c;度过了一个非常愉快的端午节。由于刚从学校回家&#xff0c;一下子伙食强度直升了个两三个档次。这也导致我的肠胃不堪重负&#xff0c;我也准备等会去健身房消耗一下盈余的热量。回到家陪伴爷爷走人生最后的…

C++11 线程库—线程操作(更新中)

前言 在C11推出线程库前&#xff0c;Windows和Linux操作系统的线程操作并不同&#xff0c;这就导致多线程程序无法跨平台&#xff0c;如果想要跨平台&#xff0c;会很麻烦并且容易出错。C11推出的线程库就解决了这一问题。 因为在Windows和Linux操作系统中有一些独特的常量&am…

OpenGL 鼠标拾取模型

1.简介 在我们的场景中&#xff0c;使用鼠标光标点击或“挑选”一个3d对象是很有用的。一种方法是从鼠标投射3d光线&#xff0c;通过相机&#xff0c;进入场景&#xff0c;然后检查光线是否与任何物体相交。这通常被称为光线投射。 我们不是从局部空间中的网格开始&#xff0c…

gRPC 实践

RPC 包管理&#xff0c;1.12前&#xff1b;旧版本要设置GO111MODULEoff&#xff1b;查找gopath/src;goroot/src&#xff1b;几乎没有包管理&#xff1b; 新版本&#xff1b;go.mod&#xff1b; module xxx go version设置GO111MODULEon 是什么 远程过程调用&#xff1b; …

Windows页面置换算法与文件操作

实验一 一、实验内容或题目&#xff1a; 随机产生页面访问序列&#xff0c;并实现LRU, FIFO, OPT三种算法进行缺页比较 二、实验目的与要求&#xff1a; 1、编写程序&#xff0c;随机产生页面访问序列&#xff0c;并实现LRU, FIFO, OPT三种算法进行缺页比较。 2、理解三种算…

自监督学习简介

1.  自监督学习 自监督学习是可以看做是一种特殊的无监督学习的一个子类别&#xff08;但并非无监督学习&#xff09;&#xff0c;因为它利用了未标记的数据。 关键思想是让模型无需手动标签即可学习数据表示。一旦模型学会了如何表示数据&#xff0c;那么它就可以用较少量的…

liunx+docker+rabbitmq安装延迟队列插件

安装版本 rabbit: RabbitMQ 3.8.16 erlang: Erlang 23.3.2 rabbit: rabbitmq_delayed_message_exchange-3.8.9-0199d11c.ez 准备 1.rabbmitMQ 安装 docker pull rabbitmq 2.rabbmitMQ 启动 docker run -d --hostname my-rabbit --name rabbit -e RABBITMQ_DEFAULT_USER用户…

10 种分布式系统必备模式

在当今的技术领域中&#xff0c;分布式系统已成为许多大型应用程序和平台的核心。构建高性能、可伸缩和可靠的分布式系统是一个复杂的挑战&#xff0c;需要合理的架构设计和模式选择。本文将介绍10个必备的分布式系统模式&#xff0c;帮助您更好地理解和应用这些模式以提升系统…

pytorch笔记:RNN 系列

来自B站视频&#xff0c;API查阅&#xff0c;TORCH.NN RNN可以处理变长序列&#xff0c;是因为其每个时刻的参数是共享的RNN每算出一个时刻都可以输出&#xff0c;适合流式输出&#xff0c;但串行计算比较慢&#xff0c;无法获取太长的历史信息RNN 初始隐状态不提供默认是0&am…

一、枚举类型——用枚举实现状态机

枚举类型很适合用来实现状态机。状态机可以处于有限数量的特定状态。它们通常根据输入&#xff0c;从一个状态移动到下一个状态&#xff0c;但同时也会存在瞬态。当任务执行完毕后&#xff0c;状态机会立即跳出所有状态。 每个状态都有某些可接受的输入&#xff0c;不同的输入…

你应该知道的 Python 自动化脚本

概要 我们都有一些需要重复做的任务。幸运的是&#xff0c;我们可以将其中一些过程自动化&#xff0c;这样我们就可以专注于做其他真正需要精力和注意力的事情。 在这篇文章中&#xff0c;我们将谈论一些 Python 自动化脚本&#xff0c;你可以轻松地用它们来执行自动化任务。重…

巨星内马尔为孕期出轨道歉了!喊话女友:“我不能想象失去你”

近日&#xff0c;巴西球星内马尔在女友布鲁娜孕期出轨的传闻引起了社会广泛关注。 22日凌晨&#xff0c;内马尔在自己的社交媒体上发文回应并道歉&#xff0c;表示自己在球场内外都会犯错&#xff0c;但私生活的问题他会在家里解决。 他还重申了自己已经为犯下的错误和不必要的…

python爬虫_函数的使用

文章目录 ⭐前言⭐python函数&#x1f496; 参数传递—值&#x1f496; 参数传递—引用&#x1f496; 多参数(*)&#x1f496;lambda匿名函数 结束 ⭐前言 大家好&#xff0c;我是yma16&#xff0c;本文分享关于python函数入门使用。 该系列文章&#xff1a; python爬虫_基本数…

【031】C++类和对象之运算符重载详解和代码实践(最全讲解)

C类和对象之运算符重载详解 引言一、运算符重载的基本概念1.1、可重载的运算符1.2、不可重载的运算符 二、重载 << 运算符&#xff08;全局函数实现&#xff09;三、重载 >> 运算符&#xff08;全局函数实现&#xff09;四、重载 运算符4.1、全局函数实现4.2、成员…