Java多线程并发synchronized 关键字

2022-11-13 10:11:03 关键字 多线程 并发

基础

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

相关文章