当前位置:

首页 > 编程开发 > java中的synchronized是什么

java中的synchronized是什么

一、synchronized实现锁的表现形式修饰实例方法,对于普通同步方法,锁是当前的实例对象修饰静态方法,对于静态同步方法,锁是当前的Class对象修饰方法代码块,对于同步方法块,锁是synchronized括号里面配置的对象!当一个线程试图访问同步代码块的时候,就必须得到锁,完成后(或者出现异常),就必须释放锁。那么锁究竟存在什么地方呢?我们一块来探究!不过,相信,既然大家能够找到这篇文章,相信大家对他的使用早已了熟于心,我们对于使用,以及为什么多线程情况下,数据会出现错乱情况,不做详细的解释!只把他

本文目录

    一、synchronized实现锁的表现形式

    1. 修饰实例方法,对于普通同步方法,锁是当前的实例对象

    2. 修饰静态方法,对于静态同步方法,锁是当前的Class对象

    3. 修饰方法代码块,对于同步方法块,锁是synchronized括号里面配置的对象!

    当一个线程试图访问同步代码块的时候,就必须得到锁,完成后(或者出现异常),就必须释放锁。那么锁究竟存在什么地方呢?我们一块来探究!

    不过,相信,既然大家能够找到这篇文章,相信大家对他的使用早已了熟于心,我们对于使用,以及为什么多线程情况下,数据会出现错乱情况,不做详细的解释!只把他的几种使用方式列出,供参考!

    ①修饰实例方法

    修饰实例方法,对于普通同步方法,锁是当前的实例对象

    这个没得说,使用的同一个实例,添加上synchronized后,线程需要排队,完成一个原子操作,但是注意前提是使用的同一个实例,他才会生效!

    正例:

    /**
    * @author huangfu
    */
    public class ExploringSynchronized implements Runnable {
       /**
        * 共享资源(临界资源)
        */
       static int i=0;
       public synchronized void add(){
           i++;
       }

       @Override
       public void run() {
           for (int j = 0; j < 100000; j++) {
               add();
           }
       }

       public static void main(String[] args) throws InterruptedException {
           ExploringSynchronized exploringSynchronized = new ExploringSynchronized();
           Thread t1 = new Thread(exploringSynchronized);
           Thread t2 = new Thread(exploringSynchronized);
           t1.start();
           t2.start();
           //join 主线程需要等待子线程完成后在结束
           t1.join();
           t2.join();
           System.out.println(i);

       }
    }

    反例:

    /**
    * @author huangfu
    */
    public class ExploringSynchronized implements Runnable {
       /**
        * 共享资源(临界资源)
        */
       static int i=0;
       public synchronized void add(){
           i++;
       }

       @Override
       public void run() {
           for (int j = 0; j < 100000; j++) {
               add();
           }
       }

       public static void main(String[] args) throws InterruptedException {
           Thread t1 = new Thread(new ExploringSynchronized());
           Thread t2 = new Thread(new ExploringSynchronized());
           t1.start();
           t2.start();
           //join 主线程需要等待子线程完成后在结束
           t1.join();
           t2.join();
           System.out.println(i);

       }
    }

    这种,即使你在方法上加上了synchronized也无济于事,因为,对于普通同步方法,锁是当前的实例对象!实例对象都不一样了,那么他们之间的锁自然也就不是同一个!

    ②修饰静态方法

    修饰静态方法,对于静态同步方法,锁是当前的Class对象

    从定义上可以看出来,他的锁是类对象,那么也就是说,以上面那个类为例:普通方法的锁对象是 new ExploringSynchronized()而静态方法对应的锁对象是ExploringSynchronized.class所以对于静态方法添加同步锁,即使你重新创建一个实例,它拿到的锁还是同一个!

    package com.byit.test;

    /**
    * @author huangfu
    */
    public class ExploringSynchronized implements Runnable {
       /**
        * 共享资源(临界资源)
        */
       static int i=0;
       public synchronized static void add(){
           i++;
       }

       @Override
       public void run() {
           for (int j = 0; j < 100000; j++) {
               add();
           }
       }

       public static void main(String[] args) throws InterruptedException {
           Thread t1 = new Thread(new ExploringSynchronized());
           Thread t2 = new Thread(new ExploringSynchronized());
           t1.start();
           t2.start();
           //join 主线程需要等待子线程完成后在结束
           t1.join();
           t2.join();
           System.out.println(i);

       }
    }

    当然,结果是我们期待的  200000

    ③修饰方法代码块

    修饰方法代码块,对于同步方法块,锁是synchronized括号里面配置的对象!

    package com.byit.test;

    /**
    * @author huangfu
    */
    public class ExploringSynchronized implements Runnable {
       /**
        * 锁标记
        */
       private static final String LOCK_MARK = "LOCK_MARK";
       /**
        * 共享资源(临界资源)
        */
       static int i=0;
       public void add(){
           synchronized (LOCK_MARK){
               i++;
           }
       }

       @Override
       public void run() {
           for (int j = 0; j < 100000; j++) {
               add();
           }
       }

       public static void main(String[] args) throws InterruptedException {
           Thread t1 = new Thread(new ExploringSynchronized());
           Thread t2 = new Thread(new ExploringSynchronized());
           t1.start();
           t2.start();
           //join 主线程需要等待子线程完成后在结束
           t1.join();
           t2.join();
           System.out.println(i);

       }
    }

    对于同步代码块,括号里面是什么,锁对象就是什么,里面可以使用this  字符串  对象等等!

    二、synchronized的底层实现

    java中synchronized的实现是基于进入和退出的 Monitor对象实现的,无论是显式同步(修饰代码块,有明确的monitorenter和 monitorexit指令)还是隐式同步(修饰方法体)!

    需要注意的是,只有修饰代码块的时候,才是基于monitorenter和 monitorexit指令来实现的;修饰方法的时候,是通过另一种方式实现的!我会放到后面去说!

    在了解整个实现底层之前,我还是希望你能够大致了解一下对象在内存中的结构详情!

    java中的synchronized是什么

    • 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

    • 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。

    这两个概念,我们简单理解就好!我们今天并不去探究对象的构成原理!我们着重探究一下对象头,他对我们理解锁尤为重要!

    一般而言,synchronized使用的锁存在于对象头里面!如果是数组对象,则虚拟机使用3个字宽存储对象,如果是非数组对象,则使用两个字宽存储对象头!字虚拟机里面1字宽等于4字节!主要结构是 Mark Word 和 Class Metadata Address组成,结构如下:

    虚拟机位数头对象结构说明
    32/64bitMark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息
    32/64bitClass Metadata Address存储到队形类型数据的指针
    32/64bit(数组)Aarray length数组的长度

    通过上述表格能够看出  锁信息 存在于 Mark Word  内,那么 Mark Word 内又是如何组成的呢?

    锁状态25bit4bit1bit是否是偏向锁2bit锁标志位
    无锁状态对象的hashcode对象的分代年龄001

    在运行起见,mark Word 里存储的数据会随着锁的标志位的变化而变化。mark Word可能变化为存储一下四种数据

    java中的synchronized是什么

    Java SE 1.6为了减少获得锁和释放锁带来的消耗,引入了偏向锁和轻量级锁,从之前上来就是重量级锁到1.6之后,锁膨胀升级的优化,极大地提高了synchronized的效率;

    锁一共有4中状态,级别从低到高:

    java中的synchronized是什么

    这几个状态会随着锁的竞争,逐渐升级。锁可以升级,但是不能降级,其根本的原因就是为了提高获取锁和释放锁的效率!

    那么,synchronized是又如何保证的线程安全的呢?或许我们需要从字节码寻找答案!

    package com.byit.test;

    /**
    * @author Administrator
    */
    public class SynText {
       private static String A = "a";
       public int i ;

       public void add(){
           synchronized (A){
               i++;
           }

       }
    }

    反编译的字节码

    Compiled from "SynText.java"
    public class com.byit.test.SynText {
     public int i;

     public com.byit.test.SynText();
       Code:
          0: aload_0
          1: invokespecial #1                  // Method java/lang/Object."":()V
          4: return

     public void add();
       Code:
          0: getstatic     #2                  // Field A:Ljava/lang/String;
          3: dup
          4: astore_1
          5: monitorenter
          6: aload_0
          7: dup
          8: getfield      #3                  // Field i:I
         11: iconst_1
         12: iadd
         13: putfield      #3                  // Field i:I
         16: aload_1
         17: monitorexit
         18: goto          26
         21: astore_2
         22: aload_1
         23: monitorexit
         24: aload_2
         25: athrow
         26: return
       Exception table:
          from    to  target type
              6    18    21   any
             21    24    21   any

     static {};
       Code:
          0: ldc           #4                  // String a
          2: putstatic     #2                  // Field A:Ljava/lang/String;
          5: return
    }

    省去不必要的,简化在简化

       5: monitorenter
         ...
         17: monitorexit
         ...
         23: monitorexit

    从字节码中可知同步语句块的实现使用的是monitorenter和 monitorexit指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令的时候,线程将试图获取对象所所对应的monitor特权,当monitor的的计数器为0的时候,线程就可以获取monitor,并将计数器设置为1.去锁成功!如果当前线程已经拥有monitor特权,则可以直接进入方法(可重入锁),计数器+1;如果其他线程已经拥有了monitor特权,那么本县城将会阻塞!

    拥有monitor特权的线程执行完成后释放monitor,并将计数器设置为0;同时执行monitorexit指令;不要担心出现异常无法执行monitorexit指令;为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

    同步代码块的原理了解了,那么同步方法如何解释?不急,我们不妨来反编译一下同步方法的状态!

    javap -verbose -p SynText > 3.txt

    代码

    package com.byit.test;

    /**
    * @author huangfu
    */
    public class SynText {
       public int i ;

       public synchronized void add(){
           i++;

       }
    }

    字节码

    Classfile /D:/2020project/byit-myth-job/demo-client/byit-demo-client/target/classes/com/byit/test/SynText.class
     Last modified 2020-1-6; size 382 bytes
     MD5 checksum e06926a20f28772b8377a940b0a4984f
     Compiled from "SynText.java"
    public class com.byit.test.SynText
     minor version: 0
     major version: 52
     flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
      #1 = Methodref          #4.#17         // java/lang/Object."":()V
      #2 = Fieldref           #3.#18         // com/byit/test/SynText.i:I
      #3 = Class              #19            // com/byit/test/SynText
      #4 = Class              #20            // java/lang/Object
      #5 = Utf8               i
      #6 = Utf8               I
      #7 = Utf8              
      #8 = Utf8               ()V
      #9 = Utf8               Code
     #10 = Utf8               LineNumberTable
     #11 = Utf8               LocalVariableTable
     #12 = Utf8               this
     #13 = Utf8               Lcom/byit/test/SynText;
     #14 = Utf8               syncTask
     #15 = Utf8               SourceFile
     #16 = Utf8               SynText.java
     #17 = NameAndType        #7:#8          // "":()V
     #18 = NameAndType        #5:#6          // i:I
     #19 = Utf8               com/byit/test/SynText
     #20 = Utf8               java/lang/Object
    {
     public int i;
       descriptor: I
       flags: ACC_PUBLIC

     public com.byit.test.SynText();
       descriptor: ()V
       flags: ACC_PUBLIC
       Code:
         stack=1, locals=1, args_size=1
            0: aload_0
            1: invokespecial #1                  // Method java/lang/Object."":()V
            4: return
         LineNumberTable:
           line 6: 0
         LocalVariableTable:
           Start  Length  Slot  Name   Signature
               0       5     0  this   Lcom/byit/test/SynText;

     public synchronized void syncTask();
       descriptor: ()V
       flags: ACC_PUBLIC, ACC_SYNCHRONIZED
       Code:
         stack=3, locals=1, args_size=1
            0: aload_0
            1: dup
            2: getfield      #2                  // Field i:I
            5: iconst_1
            6: iadd
            7: putfield      #2                  // Field i:I
           10: return
         LineNumberTable:
           line 10: 0
           line 11: 10
         LocalVariableTable:
           Start  Length  Slot  Name   Signature
               0      11     0  this   Lcom/byit/test/SynText;
    }
    SourceFile: "SynText.java"

    简化,在简化

     public synchronized void syncTask();
       descriptor: ()V
       flags: ACC_PUBLIC, ACC_SYNCHRONIZED
       Code:
         stack=3, locals=1, args_size=1
            0: aload_0
            1: dup

    我们能够看到 flags: ACC_PUBLIC, ACC_SYNCHRONIZED这样的一句话

    从字节码中可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理。

    那么在JAVA6之前,为什么synchronized会如此的慢?

    那是因为,操作系统实现线程之间的切换需要系统内核从用户态切换到核心态!这个状态之间的转换,需要较长的时间,时间成本高!所以这也就是synchronized慢的原因!

    三、锁膨胀的过程

    在这之前,你需要知道什么是锁膨胀!他是JAVA6之后新增的一个概念!是一种针对之前重量级锁的一种性能的优化!他的优化,大部分是基于经验上的一些感官,对锁来进行优化!

    ①偏向锁

    研究发现,大多数情况下,锁不仅不存在多线程竞争,而且还总是由一条线程获得!因为为了减少锁申请的次数!引进了偏向锁!在没有锁竞争的情况下,如果一个线程获取到了锁,那么锁就进入偏向锁的模式!当线程再一次请求锁时,无需申请,直接获取锁,进入方法!但是前提是没有锁竞争的情况,存在锁竞争,锁会立即膨胀,膨胀为轻量级锁!

    ②轻量级锁

    偏向锁失败,那么锁膨胀为轻量级锁!此时锁机构变为轻量级锁结构!他的经验依据是:“绝大多数情况下,在整个同步周期内,不会存在锁的竞争”,故而,轻量级锁适合,线程交替进行的场景!如果在同一时间出现两条线程对同一把锁的竞争,那么此时轻量级锁就不会生效了!但是,jdk官方为了是锁的优化性能更好,轻量级锁失效后,并不会立即膨胀为重量级锁!而是将锁转换为自旋锁状态!

    ③自旋锁

    轻量级锁失败后,为了是避免线程挂起,引起内核态的切换!为了优化,此时线程会进入自选状态!他可能会进行几十次,上百次的空轮训!为什么呢?又是经验之谈!他们认为,大多数情况下,线程持有锁的时间都不会太长!做几次空轮训,就能大概率的等待到锁!事实证明,这种优化方式确实有效!最后如果实在等不到锁!没办法,才会彻底升级为重量级锁!

    ④锁消除

    jvm在进行代码编译时,会基于上下文扫描;将一些不可能存在资源竞争的的锁给消除掉!这也是JVM对于锁的一种优化方式!不得不感叹,jdk官方的脑子!举个例子!在方法体类的局部变量对象,他永远也不可能会发生锁竞争,例如:

    /**
    * @author huangfu
    */
    public class SynText {
       public static void add(String name1 ,String name2){
           StringBuffer sb = new StringBuffer();
           sb.append(name1).append(name2);
       }

       public static void main(String[] args) {
           for (int i = 0; i < 10000000; i++) {
               add("w"+i,"q"+i);
           }
       }
    }

    不能否认,StringBuffer是线程安全的!但是他永远也不会被其他线程引用!故而,锁失效!故而,被消除掉!

    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 Java
    相关文章 更多
    Java单元测试入门:使用JUnit 5编写第一个测试用例
    Java单元测试入门:使用JUnit 5编写第一个测试用例

    面向初学者的JUnit 5实用入门指南。从测试概念、Maven依赖配置,到手把手编写首个单元测试用例及断言验证,助你掌握Java单元测试的核心流程。

    Java变量未初始化报错解析:局部变量与成员变量默认值差异
    Java变量未初始化报错解析:局部变量与成员变量默认值差异

    深入解析Java中局部变量未初始化编译报错与成员变量自动赋予默认值的机制差异,涵盖JVM内存分配、编译期确定性赋值检查与实际排查修复方案。

    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat
    Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat

    详细讲解在Java中获取当前系统时间并进行格式化输出的两种核心方案:基于Java 8全新日期时间API的DateTimeFormatter与传统遗留的SimpleDateFormat,包含完整代码示例、线程安全机制对比及生产选型建议。

    Java中final与static final常量的定义及初始化规则
    Java中final与static final常量的定义及初始化规则

    深入解析Java中final变量与static final常量的定义、内存语义、初始化时机及合法赋值路径,涵盖编译期常量优化与实战避坑指南。

    Java抽象方法与接口方法的区别及选型建议
    Java抽象方法与接口方法的区别及选型建议

    深入对比Java中抽象类抽象方法与接口方法的语法演进、设计意图及运行期差异,并提供清晰的架构选型决策流程与实战范例。

    Java实现HTTP请求与JSON解析的完整代码示例
    Java实现HTTP请求与JSON解析的完整代码示例

    本文提供Java标准库HttpClient发起HTTP GET/POST请求并结合Jackson进行JSON数据反序列化与序列化的完整代码示例与工程实践指南。

    Java递归出现StackOverflowError的原因分析及栈空间优化方案
    Java递归出现StackOverflowError的原因分析及栈空间优化方案

    详细分析Java递归调用出现StackOverflowError的原因,结合代码说明线程栈、栈帧和递归深度之间的关系,并介绍终止条件排查、迭代改写、减少栈帧开销以及通过-Xss调整JVM栈空间的方法。

    Java空指针异常(NPE)的代码排查与修复实例
    Java空指针异常(NPE)的代码排查与修复实例

    针对Java中常见的NullPointerException(NPE),本文从堆栈日志定位、典型代码触发场景、具体修复代码对比到防御体系进行系统讲解,提供实用且易上手的排查与修复方案。

    selenium上传文件教程本地文件路径设置方法
    selenium上传文件教程本地文件路径设置方法

    深入解析Selenium处理文件上传的技术细节,涵盖标准HTML控件的路径直接注入法与非标准控件的系统级模拟方案,提供Windows与Linux环境下的路径设置最佳实践及常见异常排查。

    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景
    大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景

    针对大数据工程场景,系统对比Java、Scala与Python在执行性能、生态结合、开发效率和维护成本上的核心差异,给出清晰的架构选型依据与落地方案。

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程
    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    Mac软件 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    Aperty macOS版
    Aperty macOS版
    macOS

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    WINDOWS 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    PixTV 网页版
    PixTV 网页版
    Windows

    PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。