当前位置:

首页 > 编程开发 > 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对象实现的,无论是显式同步(修饰代码块,有明确的monitorentermonitorexit指令)还是隐式同步(修饰方法体)!

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

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

java中的synchronized是什么

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

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

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

一般而言,synchronized使用的锁存在于对象头里面!如果是数组对象,则虚拟机使用3个字宽存储对象,如果是非数组对象,则使用两个字宽存储对象头!字虚拟机里面1字宽等于4字节!主要结构是 Mark WordClass 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

从字节码中可知同步语句块的实现使用的是monitorentermonitorexit指令,其中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是线程安全的!但是他永远也不会被其他线程引用!故而,锁失效!故而,被消除掉!

本文内容来源于互联网,如有侵权请联系删除。
作者最新文章
编程开发
相关文章 更多
谷歌浏览器Mac版入口
谷歌浏览器Mac版入口

谷歌浏览器Mac版官方安装指南 谷歌浏览器Mac版官方安装入口是https://www.google.com/chrome/,需macOS 12+系统、500MB空间,下载.dmg后拖入应用程序安装,支持多设备同步、性能优化与隐私保护功能。 苹果电脑Chrome的安装入口究竟在哪里?这个问题最近可是

Chrome浏览器JS脚本不运行怎么办
Chrome浏览器JS脚本不运行怎么办

Chrome中JavaScript未执行需依次检查:一、移除站点级禁用并添加允许域名;二、开启全局JavaScript开关;三、禁用干扰扩展;四、在开发者工具中启用JavaScript;五、重置内容设置为默认。 有时在Chrome里打开网页,会发现交互按钮点了没反应,数据加载不出来,页面仿佛“静止”

IE浏览器怀旧版在线网址
IE浏览器怀旧版在线网址

IE浏览器怀旧版在线网址:一次精准的技术时光回溯 最近,不少老用户和怀旧爱好者在反复搜索一个问题:那个经典的Internet Explorer,如今还能在哪里原汁原味地体验到?答案指向一个特定的地址:https://ie.microsoft.com/legacy/。 这个网站远不止是一个简单的“皮肤

火狐浏览器有哪些设置功能
火狐浏览器有哪些设置功能

火狐浏览器五大核心设置功能:解锁高效、安全与个性化体验 火狐浏览器功能强大,但如果不仔细挖掘,很多能大幅提升效率和安全性的设置可能就“藏着掖着”了。这就好比拥有一台高性能设备,却只用了基础模式。那么,如何把它调整到最顺手、最安全的状态?接下来,我们就聚焦于当前版本(截至2025年末)最关键的五大设置

chrome搜索免验证入口
chrome搜索免验证入口

Chrome官方免验证入口为https://www.google.cn/chrome/,提供全平台安装包、免登录即用、本地化安全机制及引擎级性能优化。 到底该去哪里找正版、免费且无需繁琐验证的Chrome浏览器入口?这个问题困扰了不少网友。今天,我们就来直通核心,为大家详细拆解Chrome引擎的官方

java heap space 选型思路:使用场景与区别整理
java heap space 选型思路:使用场景与区别整理

Java堆是JVM存储对象的核心内存区域,配置需结合场景:单体应用适中设置;大数据处理需大堆并关注GC停顿;微服务强调快速启动;高并发需精细划分堆区域。关键参数-Xms和-Xmx建议等值以稳定性能。垃圾回收器选择影响效率,如G1适用于大堆,ZGC可实现低停顿。内存错误时需监控堆状态。

java heap space 使用中遇到的问题怎么解决
java heap space 使用中遇到的问题怎么解决

Java堆内存溢出错误通常因内存泄漏、数据处理需求过大或JVM参数配置不当引起。排查时可借助jmap、堆转储及MAT等工具定位问题。解决方案包括调整JVM内存参数(如-Xmx)、修复代码中的内存泄漏、优化大数据处理逻辑,并建立持续监控与预防机制,以保障应用稳定运行。

java xml 选型思路:使用场景与区别整理
java xml 选型思路:使用场景与区别整理

XML在Java开发中用于配置、数据交换等场景。解析方式主要有DOM、SAX、StAX及第三方库。DOM适合操作小文件,SAX/StAX适合处理大文件流,JAXB用于对象与XML映射。选型需结合数据大小、内存、性能及团队熟悉度,现代框架常封装底层解析。

using namespace 使用中遇到的问题怎么解决
using namespace 使用中遇到的问题怎么解决

命名空间的基本概念与常见引入问题在C++等编程语言中,命名空间(namespace)是一种将代码标识符(如变量、函数、类名)封装在特定名称下的机制,其主要目的是避免命名冲突,尤其是在大型项目或使用多个第三方库时。使用“using namespace”指令可以将指定命名空间中的所有名称引入当前作用域,

c语言函数递归 实操经验总结:这些技巧很实用
c语言函数递归 实操经验总结:这些技巧很实用

理解递归的基本原理在C语言中,递归是一种函数调用自身的编程技术。要掌握它,首先需要理解其核心思想:将一个复杂的大问题,分解为一个或几个与原问题相似但规模更小的子问题,直到子问题足够简单,可以直接求解。这个过程通常包含两个关键部分:递归出口和递归体。递归出口定义了问题何时不再继续分解,即最简单、可直接

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

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

Windows
Windows

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

macOS软件
macOS软件

正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

Mac软件 更多
灵活计算器
灵活计算器
macOS/iOS/Android

灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

赤友清理大师
赤友清理大师
macOS

赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

WINDOWS 更多
Windows 10
Windows 10
Windows

Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

密码键盘
密码键盘
Windows/macOS/iOS/Android

密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。