Java多线程并发synchronized 关键字
作者: 自动化BUG制造器
基础
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
从这里看,的确是出现了两组 monitorenter
和monitorexit
。
而从静态方法的 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内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!