Java多线程并发synchronized 关键字

目录
  • 基础
    • 修饰普通方法
    • 修饰静态方法
    • Synchronized 加锁原理
    • monitorenter
    • monitorexit
    • synchronized 修饰静态方法
    • 优点、缺点及优化
    • 其他说明

基础

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内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java中提供synchronized后为什么还要提供Lock

    目录 一.为何提供Lock接口? 二.死锁问题 三.synchronized的局限性 四.解决问题  摘要: 在Java中提供了synchronized关键字来保证只有一个线程能够访问同步代码块.既然已经提供了synchronized关键字,那为何在Java的SDK包中,还会提供Lock接口呢?这是不是重复造轮子,多此一举呢?今天,我们就一起来探讨下这个问题. 在Java中提供了synchronized关键字来保证只有一个线程能够访问同步代码块.既然已经提供了synchronized关键字,那为

  • Java多线程并发编程 Synchronized关键字

    synchronized 关键字解析 同步锁依赖于对象,每个对象都有一个同步锁. 现有一成员变量 Test,当线程 A 调用 Test 的 synchronized 方法,线程 A 获得 Test 的同步锁,同时,线程 B 也去调用 Test 的 synchronized 方法,此时线程 B 无法获得 Test 的同步锁,必须等待线程 A 释放 Test 的同步锁才能获得从而执行对应方法的代码. 综上,正确使用 synchronized 关键字可确保原子性. synchronized 关键字的特

  • Java对象级别与类级别的同步锁synchronized语法示例

    目录 1.对象级别的同步锁 2.类级别的同步锁 3.总结 Java synchronized 关键字 可以将一个代码块或一个方法标记为同步代码块.同步代码块是指同一时间只能有一个线程执行的代码,并且执行该代码的线程持有同步锁.synchronized关键字可以作用于 一个代码块 一种方法 当一个方法或代码块被声明为synchronized时,如果一个线程正在执行该synchronized 方法或代码块,其他线程会被阻塞,直到持有同步锁的线程释放.根据锁定的范围可以分为 类级别的锁可以防止多个线程

  • Java多线程之synchronized同步代码块详解

    目录 1.同步方法和同步块,哪种更好? 2.synchronized同步代码块 3.如果同步块内的线程抛出异常会发生什么? 总结 面试题: 1同步方法和同步块,哪种更好? 2.如果同步块内的线程抛出异常会发生什么? 1. 同步方法和同步块,哪种更好? 同步块更好,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率.请知道一条原则:同步的范围越小越好. 对于小的临界区,我们直接在方法声明中设置synchronized同步关键字,可以避免竞态条件的问题.但是对于较大的临界区代码段

  • Java synchronized同步方法详解

    目录 1.synchronized同步方法 2.synchronized方法将对象作为锁 3.多个锁对象 4.如果同步方法内的线程抛出异常会发生什么? 5.静态的同步方法 总结 面试题: 1.如何保证多线程下 i++ 结果正确? 2.一个线程如果出现了运行时异常会怎么样? 3.一个线程运行时发生异常会怎样? 为了避免临界区的竞态条件发生,有多种手段可以达到目的. (1) 阻塞式的解决方案:synchronized,Lock (2) 非阻塞式的解决方案:原子变量 synchronized 即俗称的

  • Java并发系列之JUC中的Lock锁与synchronized同步代码块问题

    目录 一.Lock锁 二.锁的底层 三.案例 案例一:传统的synchronized实现 案例二:Lock锁的实现 四.Lock锁和synchronized的区别 写在前边: 在Java服务端中,会常常遇到并发的场景,以下我使用两个售票的案例实现传统的Lock锁与synchronized加锁解决线程安全问题. 本章代码:Gitee: juc.demo 一.Lock锁 ReentrantLock类: 可重用锁(公平锁|非公平锁) ReentrantReadWriteLock.ReadLock:读锁

  • Java中synchronized用法汇总

    目录 用法简介 1.修饰普通方法 2.修饰静态方法 3.修饰代码块 总结 在 Java 语言中,保证线程安全性的主要手段是加锁,而 Java 中的锁主要有两种:synchronized 和 Lock,我们今天重点来看一下 synchronized 的几种用法. 用法简介 使用 synchronized 无需手动执行加锁和释放锁的操作,我们只需要声明 synchronized 关键字就可以了,JVM 层面会帮我们自动的进行加锁和释放锁的操作.synchronized 可用于修饰普通方法.静态方法和

  • Java 深入浅出分析Synchronized原理与Callable接口

    目录 一.基本特点 二.加锁工作过程 偏向锁 轻量级锁 重量级锁 三.其他的优化操作 锁消除 锁粗化 四.Callable 接口 一.基本特点 1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁. 2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁. 3. 实现轻量级锁的时候大概率用到的自旋锁策略 4. 是一种不公平锁 5. 是一种可重入锁 6. 不是读写锁 二.加锁工作过程 JVM 将 synchronized 锁分为 无锁.偏向锁.轻量级锁.重量级锁状态.会根据情况

  • Java多线程并发synchronized 关键字

    目录 基础 修饰普通方法 修饰静态方法 Synchronized 加锁原理 monitorenter monitorexit synchronized 修饰静态方法 优点.缺点及优化 其他说明 基础 Java 在虚拟机层面提供了 synchronized 关键字供开发者快速实现互斥同步的重量级锁来保障线程安全. synchronized 关键字可用于两种场景: 修饰方法. 持有一个对象,并执行一个代码块. 而根据加锁的对象不同,又分为两种情况: 对象锁 类对象锁 以下代码示例是 synchron

  • Java多线程并发编程 Volatile关键字

    volatile 关键字是一个神秘的关键字,也许在 J2EE 上的 JAVA 程序员会了解多一点,但在 Android 上的 JAVA 程序员大多不了解这个关键字.只要稍了解不当就好容易导致一些并发上的错误发生,例如好多人把 volatile 理解成变量的锁.(并不是) volatile 的特性: 具备可见性 保证不同线程对被 volatile 修饰的变量的可见性. 有一被 volatile 修饰的变量 i,在一个线程中修改了此变量 i,对于其他线程来说 i 的修改是立即可见的. 如: vola

  • Java 多线程并发编程_动力节点Java学院整理

    一.多线程 1.操作系统有两个容易混淆的概念,进程和线程. 进程:一个计算机程序的运行实例,包含了需要执行的指令:有自己的独立地址空间,包含程序内容和数据:不同进程的地址空间是互相隔离的:进程拥有各种资源和状态信息,包括打开的文件.子进程和信号处理. 线程:表示程序的执行流程,是CPU调度执行的基本单位:线程有自己的程序计数器.寄存器.堆栈和帧.同一进程中的线程共用相同的地址空间,同时共享进进程锁拥有的内存和其他资源. 2.Java标准库提供了进程和线程相关的API,进程主要包括表示进程的jav

  • Java多线程并发编程(互斥锁Reentrant Lock)

    Java 中的锁通常分为两种: 通过关键字 synchronized 获取的锁,我们称为同步锁,上一篇有介绍到:Java 多线程并发编程 Synchronized 关键字. java.util.concurrent(JUC)包里的锁,如通过继承接口 Lock 而实现的 ReentrantLock(互斥锁),继承 ReadWriteLock 实现的 ReentrantReadWriteLock(读写锁). 本篇主要介绍 ReentrantLock(互斥锁). ReentrantLock(互斥锁)

  • Java多线程并发编程和锁原理解析

    这篇文章主要介绍了Java多线程并发编程和锁原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.前言 最近项目遇到多线程并发的情景(并发抢单&恢复库存并行),代码在正常情况下运行没有什么问题,在高并发压测下会出现:库存超发/总库存与sku库存对不上等各种问题. 在运用了 限流/加锁等方案后,问题得到解决. 加锁方案见下文. 二.乐观锁 & 悲观锁 1.乐观锁 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁

  • Java 多线程并发ReentrantLock

    目录 背景 ReentrantLock 可重入特性 公平锁设置参数 源码分析 Lock 接口 加锁操作 内部类 Sync tryLock initialTryLock lock lockInterruptibly tryLockNanos tryRelease newCondition NonfairSync 非公平锁 FairSync 构造函数 核心属性和方法 总结 背景 在 Java 中实现线程安全的传统方式是 synchronized 关键字,虽然它提供了一定的同步能力,但它在使用上是严格

  • Java 多线程并发AbstractQueuedSynchronizer详情

    目录 AbstractQueuedSynchronizer 核心思想 为什么需要 AQS 用法 用法示例 AQS 底层原理 父类 AbstractOwnableSynchronizer CLH 队列 Condition 用于等待的方法 用于唤醒的方法 ConditionObject Signalling methods Waiting methods enableWait canReacquire unlinkCancelledWaiters 对外提供的等待方法 awaitUninterrupt

  • Java 多线程并发LockSupport

    目录 概览 源码分析 静态方法 Blocker unpark Unsafe 的 unpark 方法 park 不带 blocker 参数的分组 需要 blocker 参数的分组 park/unpark 和 Object 的 wait/notify 区别 概览 这部分内容来自于这个类的注释,简单翻译了下. LockSupport 类是用于创建锁和其他同步类的基本线程阻塞原语. 它的实现思想是给每个使用它的线程颁发一个许可,当许可是可用状态时(线程有许可),调用 park 方法会消耗一个许可,方法立

  • Java多线程并发开发之DelayQueue使用示例

    在学习Java 多线程并发开发过程中,了解到DelayQueue类的主要作用:是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走.这种队列是有序的,即队头对象的延迟到期时间最长.注意:不能将null元素放置到这种队列中. Delayed,一种混合风格的接口,用来标记那些应该在给定延迟时间之后执行的对象.此接口的实现必须定义一个 compareTo 方法,该方法提供与此接口的 getDelay 方法一致的排序. 在网上看到了一些

  • Java中的synchronized关键字

    目录 1.synchronized锁的底层实现原理 2.基于synchronized实现单例模式 3.利用类加载实现单例模式(饿汉模式) 1.synchronized锁的底层实现原理 JVM基于进入和退出Monitor对象来实现方法同步和代码块同步.代码块同步是使用monitorenter和monitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处.任何对象都有一个monitor与之关联,当且一个moni

随机推荐