基础

Java 在虚拟机层面提供了 synchronized 关键字供开发者快速实现互斥同步的重量级锁来保障线程安全。

synchronized 关键字可用于两种场景:

  • 修饰方法。
  • 持有一个对象,并执行一个代码块。

而根据加锁的对象不同,又分为两种情况:

  • 对象锁
  • 类对象锁

以下代码示例是 synchronized 的具体用法:

1. 修饰方法
synchronized void function() { ... }
2. 修饰静态方法
static synchronized void function() { ... }
3. 对对象加锁
synchronized(object) {
    // ...
}

修饰普通方法

synchronized 修饰方法加锁,相当于对当前对象加锁,类 A 中的 function() 是一个 synchronized 修饰的普通方法:

class A {
    synchronized void function() { ... }
}

它等效于:

class A {
    void function() { 
        synchronized(this) { ... }
    }
}

结论synchronized 修饰普通方法,实际上是对当前对象进行加锁处理,也就是对象锁。

修饰静态方法

synchronized 修饰静态方法,相当于对静态方法所属类的 class 对象进行加锁,这里的 class 对象是 JVM 在进行类加载时创建的代表当前类的 java.lang.Class 对象,每个类都有唯一的 Class 对象。这种对 Class 对象加锁,称之为类对象锁

类加载阶段主要做了三件事情:

根据特定名称查找类或接口类型的二进制字节流。

将这个二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构。

在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

class A {
    static synchronized void function() { ... }
    // 相当于对 class 对象加锁,这里只是描述,静态方法和普通方法不可等效。
    void function() {
        synchronized(A.class) { ... }
    }
}

也就是说,如果一个普通方法中持有了 A.class ,那么就会与静态方法 function() 互斥,因为本质上它们加锁的对象是同一个。

Synchronized 加锁原理

public class Sync {
    Object lock = new Object();
    public void function() {
        synchronized (lock) {
            System.out.print("lock");
        }
    }
}

这是一个简单的 synchronized 关键字对 lock 对象进行加锁的 demo ,经过javac Sync.java 命令反编译生成 class 文件,然后通过 javap -verbose Sync 命令查看内容:

  public void function();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: getfield      #7                  // Field lock:Ljava/lang/Object;
         4: dup
         5: astore_1
         6: monitorenter                      // 【1】
         7: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
        10: ldc           #19                 // String lock
        12: invokevirtual #20                 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
        15: aload_1
        16: monitorexit                       // 【2】
        17: goto          25
        20: astore_2
        21: aload_1
        22: monitorexit                       // 【3】
        23: aload_2
        24: athrow
        25: return

【1】与【2】处的 monitorenter 和 monitorexit 两个指令就是加锁操作的关键。

而【3】处的 monitorexit ,是为了保证在同步代码块中出现 Exception 或者 Error 时,通过调用第二个monitorexit 指令来保证释放锁。

monitorenter 指令会让对象在对象头中的锁计数器计数 1, monitorexit 指令则相反,计数器 - 1。

monitor 锁的底层逻辑

对象会关联一个 monitor ,monitorenter 指令会检查对象是否管理了 monitor 如果没有创建一个 ,并将其关联到这个对象。

monitor 内部有两个重要的成员变量 owner(拥有这把锁的线程)和 recursions(记录线程拥有锁的次数),当一个线程拥有 monitor 后其他线程只能等待。

加锁意味着在同一时间内,对象只能被一个线程获取到。

monitorenter

monitorenter 指令标记了同步代码块的开始位置,也就是这个时候会创建一个 monitor ,然后当前线程会尝试获取这个 monitor 。

monitorenter 指令触发时,线程尝试获取 monitor 锁有三种逻辑:

  • monitor 锁计数器为 0 ,意味着目前还没有被任意线程持有,那这个线程就会立刻持有这个 monitor 锁,然后把锁计数器 1,一旦 1,别的线程再想获取,就需要等待。
  • 如果又对当前对象执行了一个 monitorenter 指令,那么对象关联的 monitor 已经存在,就会把锁计数器 1,锁计数器的值此时是 2,并且随着重入的次数,会一直累加。
  • monitor 锁已被其他线程持有,锁计数器不为 0 ,当前线程等待锁释放。

monitorexit

monitorexit 指令会对锁计数器进行 - 1 ,如果在执行 - 1 后锁计数器仍不为 0 ,持有锁的线程仍持有这个锁,直到锁计数器等于 0 ,持有线程才释放了锁。

任意线程访问加锁对象时,首先要获取对象的 monitor ,如果获取失败,该现场进入阻塞状态,即 Blocked。当这个对象的 monitor 被持有线程释放后,阻塞等待的线程就有机会获取到这个 monitor 。

synchronized 修饰静态方法

根据锁计数器的原理,理论上说, monitorenter 和 monitorexit 两个指令应该成对出现(抛除处理 Exception 或 Error 的 monitorexit)。重复对同一个线程进行加锁。

我们来写一个示例检查一下:

public class Sync {
    Object lock = new Object();
    public void function() {
        synchronized (Sync.class) {
            System.out.print("lock");
            method();
        }
    }
    synchronized static void method() {
        System.out.print("method");
    };
}

synchronized (Sync.class) 先持有了 Sync 的类对象,然后再通过 synchronized 静态方法进行一次加锁,理论上说,反编译后应该是出现两对 monitorenter 和 monitorexit ,查看反编译 class 文件:

  public void function();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #8                  // class javatest/Sync
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #19                 // String lock
        10: invokevirtual #20                 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
        13: invokestatic  #26                 // Method method:()V
        16: aload_1
        17: monitorexit
        18: goto          26
        21: astore_2
        22: aload_1
        23: monitorexit
        24: aload_2
        25: athrow
        26: return

method方法的字节码:

  static synchronized void method();
    descriptor: ()V
    flags: (0x0028) ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #29                 // String method
         5: invokevirtual #20                 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
         8: return

神奇的现象出现了,monitorenter 出现了一次, monitorexit 出现了两次,这和我们最开始只加一次锁的 demo 一致了。

那么是不是因为静态方法的原因呢,我们将 demo 改造成下面的效果:

public class Sync {
    public void function() {
        synchronized (Sync.class) {
            System.out.print("lock");
        }
        method();
    }
    void method() {
        synchronized (Sync.class) {
            System.out.print("method");
        }
    }
}

反编译结果:

  public void function();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #7                  // class javatest/Sync
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #15                 // String lock
        10: invokevirtual #17                 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
        13: aload_0
        14: invokevirtual #23                 // Method method:()V
        17: aload_1
        18: monitorexit
        19: goto          27
        22: astore_2
        23: aload_1
        24: monitorexit
        25: aload_2
        26: athrow
        27: return

method 方法的编译结果:

  void method();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #7                  // class javatest/Sync
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #26                 // String method
        10: invokevirtual #17                 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
        13: aload_1
        14: monitorexit
        15: goto          23
        18: astore_2
        19: aload_1
        20: monitorexit
        21: aload_2
        22: athrow
        23: return

从这里看,的确是出现了两组 monitorentermonitorexit 。

而从静态方法的 flags: (0x0028) ACC_STATIC, ACC_SYNCHRONIZED 中,我们可以看出,JVM 对于同步静态方法并不是通过monitorenter和 monitorexit 实现的,而是通过方法的 flags 中添加 ACC_SYNCHRONIZED 标记实现的。

而如果换一种方式,不使用嵌套加锁,改为连续执行两次对同一个对象加锁解锁:

public void function() {
    synchronized (Sync.class) {
        System.out.print("lock");
    }
    method();
}

反编译:

public void function();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #7                  // class javatest/Sync
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #15                 // String lock
        10: invokevirtual #17                 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
        13: aload_1
        14: monitorexit
        15: goto          23
        18: astore_2
        19: aload_1
        20: monitorexit
        21: aload_2
        22: athrow
        23: aload_0
        24: invokevirtual #23                 // Method method:()V
        27: return

method 方法的编译结果是:

  void method();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #7                  // class javatest/Sync
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #26                 // String method
        10: invokevirtual #17                 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
        13: aload_1
        14: monitorexit
        15: goto          23
        18: astore_2
        19: aload_1
        20: monitorexit
        21: aload_2
        22: athrow
        23: return

看来结果也是一样的,monitorenter 和 monitorexit 成对出现。

优点、缺点及优化

synchronized 关键字是 JVM 提供的 API ,是重量级锁,所以它具有重量级锁的优点,保持严格的互斥同步。

而缺点则同样是互斥同步的角度来说的:

  • 效率低:锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock 可以中断和设置超时。
  • 不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象)。

优化方案:Java 提供了java.util.concurrent 包,其中 Lock 相关的一些 API ,拓展了很多功能,可以考虑使用 J.U.C 中丰富的锁机制实现来替代 synchronized

其他说明

最后,本文环境基于:

java version "14.0.1" 2020-04-14
Java(TM) SE Runtime Environment (build 14.0.1 7)
Java HotSpot(TM) 64-Bit Server VM (build 14.0.1 7, mixed mode, sharing)
JDK version 1.8.0_312

到此这篇关于Java多线程并发synchronized 关键字的文章就介绍到这了,更多相关Java synchronized内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

Java多线程并发synchronized 关键字的更多相关文章

  1. iOS:核心图像和多线程应用程序

    我试图以最有效的方式运行一些核心图像过滤器.试图避免内存警告和崩溃,这是我在渲染大图像时得到的.我正在看Apple的核心图像编程指南.关于多线程,它说:“每个线程必须创建自己的CIFilter对象.否则,你的应用程序可能会出现意外行为.”这是什么意思?我实际上是试图在后台线程上运行我的过滤器,所以我可以在主线程上运行HUD(见下文).这在coreImage的上下文中是否有意义?

  2. ios – 意外的核心数据多线程违规

    我正在使用苹果的并发核心数据调试器.-com.apple.CoreData.ConcurrencyDebug1有时候我得到__Multithreading_Violation_AllThatIsLeftToUsIsHonor__,即使我几乎肯定线程没有被违反.这是发生异常的代码的一部分(代码是扩展NSManagedobject的协议的一部分):代码在上下文的执行:块中执行.这里是线程信息:和调试器

  3. ios – UIGraphicsBeginImageContextWithOptions和多线程

    我对UIGraphicsBeginImageContextWithOptions和线程有点困惑,因为根据UIKitFunctionReferenceUIGraphicsBeginImageContextWithOptions应该只在主线程上调用.当被调用时,它创建一个基于位图的上下文,可以使用CoreGraphics的函数或者像-drawInRect这样的方法来处理:对于UIImage,-draw

  4. ios – 为自定义创建的串行异步队列设置优先级

    如何使用GCD为自定义创建的串行异步队列设置高优先级?如果是这样,什么是替代解决方案?解决方法您的队列仍然是串行的.它只会在高优先级全局并发后台队列的一个插槽中一次执行一项任务.一旦创建,串行队列就不能以任何方式“并发”.同样,如果您创建并发队列并将其设置为以串行队列为目标,则它实际上变为串行.这一切都在manpage中有所涉及.

  5. Swift之dispatch_source实现多线程定时关闭功能

    由于在项目中需要用到定时关闭音频功能,本来打算用NSTimer的,可是写起来并不是那么精简好用,所以又在网上找到相关的实例,结合自己项目需要,就写出了如下代码,还请大家指教,废话不多说:

  6. swift 多线程实现

  7. Swift线程安全详解-概念,三种锁,死锁,Atomic,synchronized

    研究了下,是线程安全问题。UIKit以及Fundation事实上,大多数Cocoa提供的Api都不是线程安全的,尤其是与UI相关的UIKit,只能在主线程上操作。需要线程安全的时候,开发者自己维护就可以了。用来加锁,解锁。关于ObjectiveC参考这篇文章Atomic一个非Atomic的属性在非ARC的时候像这样可以看到,如果在多线程同时set的情况下,可能会造成release两次。Property的Runtime对应的C代码为可以看到,如果是nonatomic的,synchronized可以看看这个S

  8. swift_多线程基础_最简单用法GCD, NSOperationQueue, NSThread

    ////ViewController.swift//study1-1//Createdbyadminon15/12/28.//copyright2015年admin.Allrightsreserved.//importUIKitclassViewController:UIViewController{@IBOutletvarmyLable:UILabel?@IBActionfuncclickBut

  9. swift__多线程GCD详解

    有以下*-disPATCH_QUEUE_PRIORITY_HIGH:*-disPATCH_QUEUE_PRIORITY_DEFAULT:多用默认*-disPATCH_QUEUE_PRIORITY_LOW:*-disPATCH_QUEUE_PRIORITY_BACKGROUND:*第二个参数为预留参数,一般为0*/letmyQueue:dispatch_queue_t=dispatch_get_global_queue//用异步的方式运行队列里的任务dispatch_async//-------------

  10. Swift - 多线程实现方式3 - Grand Central DispatchGCD

    dispatchqueue可以是并发的或串行的。dispatch_suspend后,追加到DispatchQueue中尚未执行的任务在此之后停止执行。6//创建并行队列conQueue:dispatch_queue_t=dispatch_queue_create//暂停一个队列dispatch_suspend//继续队列dispatch_resume6,dispatch_once一次执行保证dispatch_once中的代码块在应用程序里面只执行一次,无论是不是多线程。注意,我们不能(直接)取消我们已经提

随机推荐

  1. 基于EJB技术的商务预订系统的开发

    用EJB结构开发的应用程序是可伸缩的、事务型的、多用户安全的。总的来说,EJB是一个组件事务监控的标准服务器端的组件模型。基于EJB技术的系统结构模型EJB结构是一个服务端组件结构,是一个层次性结构,其结构模型如图1所示。图2:商务预订系统的构架EntityBean是为了现实世界的对象建造的模型,这些对象通常是数据库的一些持久记录。

  2. Java利用POI实现导入导出Excel表格

    这篇文章主要为大家详细介绍了Java利用POI实现导入导出Excel表格,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  3. Mybatis分页插件PageHelper手写实现示例

    这篇文章主要为大家介绍了Mybatis分页插件PageHelper手写实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  4. (jsp/html)网页上嵌入播放器(常用播放器代码整理)

    网页上嵌入播放器,只要在HTML上添加以上代码就OK了,下面整理了一些常用的播放器代码,总有一款适合你,感兴趣的朋友可以参考下哈,希望对你有所帮助

  5. Java 阻塞队列BlockingQueue详解

    本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景,通过实例代码介绍了Java 阻塞队列BlockingQueue的相关知识,需要的朋友可以参考下

  6. Java异常Exception详细讲解

    异常就是不正常,比如当我们身体出现了异常我们会根据身体情况选择喝开水、吃药、看病、等 异常处理方法。 java异常处理机制是我们java语言使用异常处理机制为程序提供了错误处理的能力,程序出现的错误,程序可以安全的退出,以保证程序正常的运行等

  7. Java Bean 作用域及它的几种类型介绍

    这篇文章主要介绍了Java Bean作用域及它的几种类型介绍,Spring框架作为一个管理Bean的IoC容器,那么Bean自然是Spring中的重要资源了,那Bean的作用域又是什么,接下来我们一起进入文章详细学习吧

  8. 面试突击之跨域问题的解决方案详解

    跨域问题本质是浏览器的一种保护机制,它的初衷是为了保证用户的安全,防止恶意网站窃取数据。那怎么解决这个问题呢?接下来我们一起来看

  9. Mybatis-Plus接口BaseMapper与Services使用详解

    这篇文章主要为大家介绍了Mybatis-Plus接口BaseMapper与Services使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  10. mybatis-plus雪花算法增强idworker的实现

    今天聊聊在mybatis-plus中引入分布式ID生成框架idworker,进一步增强实现生成分布式唯一ID,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部