Java 在虚拟机层面提供了 synchronized 关键字供开发者快速实现互斥同步的重量级锁来保障线程安全。
synchronized 关键字可用于两种场景:
修饰方法。持有一个对象,并执行一个代码块。而根据加锁的对象不同,又分为两种情况:
对象锁类对象锁以下代码示例是 synchronized 的具体用法:
修饰普通方法synchronized 修饰方法加锁,相当于对当前对象加锁,类 A 中的 function() 是一个 synchronized 修饰的普通方法:
它等效于:
结论:synchronized 修饰普通方法,实际上是对当前对象进行加锁处理,也就是对象锁。
修饰静态方法synchronized 修饰静态方法,相当于对静态方法所属类的 class 对象进行加锁,这里的 class 对象是 JVM 在进行类加载时创建的代表当前类的 java.lang.Class 对象,每个类都有唯一的 Class 对象。这种对 Class 对象加锁,称之为类对象锁。
类加载阶段主要做了三件事情:
根据特定名称查找类或接口类型的二进制字节流。
将这个二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
也就是说,如果一个普通方法中持有了 A.class ,那么就会与静态方法 function() 互斥,因为本质上它们加锁的对象是同一个。
Synchronized 加锁原理这是一个简单的 synchronized 关键字对 lock 对象进行加锁的 demo ,经过javac Sync.java 命令反编译生成 class 文件,然后通过 javap -verbose Sync 命令查看内容:
【1】与【2】处的 monitorenter 和 monitorexit 两个指令就是加锁操作的关键。
而【3】处的 monitorexit ,是为了保证在同步代码块中出现 Exception 或者 Error 时,通过调用第二个monitorexit 指令来保证释放锁。
monitorenter 指令会让对象在对象头中的锁计数器计数 + 1, monitorexit 指令则相反,计数器 - 1。
monitor 锁的底层逻辑
对象会关联一个 monitor ,monitorenter 指令会检查对象是否管理了 monitor 如果没有创建一个 ,并将其关联到这个对象。
monitor 内部有两个重要的成员变量 owner(拥有这把锁的线程)和 recursions(记录线程拥有锁的次数),当一个线程拥有 monitor 后其他线程只能等待。
加锁意味着在同一时间内,对象只能被一个线程获取到。
monitorentermonitorenter 指令标记了同步代码块的开始位置,也就是这个时候会创建一个 monitor ,然后当前线程会尝试获取这个 monitor 。
monitorenter 指令触发时,线程尝试获取 monitor 锁有三种逻辑:
monitor 锁计数器为 0 ,意味着目前还没有被任意线程持有,那这个线程就会立刻持有这个 monitor 锁,然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待。如果又对当前对象执行了一个 monitorenter 指令,那么对象关联的 monitor 已经存在,就会把锁计数器 + 1,锁计数器的值此时是 2,并且随着重入的次数,会一直累加。monitor 锁已被其他线程持有,锁计数器不为 0 ,当前线程等待锁释放。 monitorexitmonitorexit 指令会对锁计数器进行 - 1 ,如果在执行 - 1 后锁计数器仍不为 0 ,持有锁的线程仍持有这个锁,直到锁计数器等于 0 ,持有线程才释放了锁。
任意线程访问加锁对象时,首先要获取对象的 monitor ,如果获取失败,该现场进入阻塞状态,即 Blocked。当这个对象的 monitor 被持有线程释放后,阻塞等待的线程就有机会获取到这个 monitor 。
synchronized 修饰静态方法根据锁计数器的原理,理论上说, monitorenter 和 monitorexit 两个指令应该成对出现(抛除处理 Exception 或 Error 的 monitorexit)。重复对同一个线程进行加锁。
我们来写一个示例检查一下:
synchronized (Sync.class) 先持有了 Sync 的类对象,然后再通过 synchronized 静态方法进行一次加锁,理论上说,反编译后应该是出现两对 monitorenter 和 monitorexit ,查看反编译 class 文件:
method方法的字节码:
神奇的现象出现了,monitorenter 出现了一次, monitorexit 出现了两次,这和我们最开始只加一次锁的 demo 一致了。
那么是不是因为静态方法的原因呢,我们将 demo 改造成下面的效果:
反编译结果:
method 方法的编译结果:
从这里看,的确是出现了两组 monitorenter和monitorexit 。
而从静态方法的 flags: (0x0028) ACC_STATIC, ACC_SYNCHRONIZED 中,我们可以看出,JVM 对于同步静态方法并不是通过monitorenter和 monitorexit 实现的,而是通过方法的 flags 中添加 ACC_SYNCHRONIZED 标记实现的。
而如果换一种方式,不使用嵌套加锁,改为连续执行两次对同一个对象加锁解锁:
反编译:
method 方法的编译结果是:
看来结果也是一样的,monitorenter 和 monitorexit 成对出现。
优点、缺点及优化synchronized 关键字是 JVM 提供的 API ,是重量级锁,所以它具有重量级锁的优点,保持严格的互斥同步。
而缺点则同样是互斥同步的角度来说的:
效率低:锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock 可以中断和设置超时。不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象)。优化方案:Java 提供了java.util.concurrent 包,其中 Lock 相关的一些 API ,拓展了很多功能,可以考虑使用 J.U.C 中丰富的锁机制实现来替代 synchronized。
其他说明最后,本文环境基于:
到此这篇关于Java多线程并发synchronized 关键字的文章就介绍到这了,更多相关Java synchronized内容请搜索七叶笔记以前的文章或继续浏览下面的相关文章希望大家以后多多支持七叶笔记!