Java锁
1.介绍
首先, java 的锁分为两类:
- 第一类是 synchronized 同步关键字,这个关键字属于隐式的锁,是 jvm 层面实现,使用的时候看不见;
- 第二类是在 jdk5 后增加的 Lock 接口以及对应的各种实现类,这属于显式的锁,就是我们能在代码层面看到锁这个对象,而这些个对象的方法实现,大都是直接依赖 CPU 指令的,无关 jvm 的实现
接下来就从 synchronized 和 Lock 两方面来讲
2.synsychronized
2.1synsychronized的使用
- 如果修饰的是
具体对象
:锁的是对象
- 如果修饰的是
成员方法
:那锁的就是this
- 如果修饰的是
静态方法
:锁的就是这个对象.class
2.2 Java的对象头和monitor
理解 synchronized 原理之前,我们需要补充一下 java 对象的知识
对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充
- 对象头。Hot Spot 虚拟机对象的对象头部分包括两类信息。第一类是用于存储对象自身的运行时数据,如哈希码( Hash Code)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称它为“ Mark Word”。
对象需要存储的运行时数据很多,其实已经超出了32、64位 Bitmap 结构所能记录的最大限度,但对象头里的信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽量多的数据,根据对象的状态复用自己的存储空间
- 实例数据。实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。
这部分的存储顺序会受到虚拟机分配策略参数 (-XX: Fields Allocation Style参数) 和字段在Java源码中定义顺序的影响。Hot Spot 虚拟机默认的分配顺序为 longs/doubles、ints、shorts/chars、bytes/booleans、oops( Ordinary Object Pointers,OOPs),从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果 Hotspot 虚拟机的 XX: Compact Fields 参数值为 true(默认就为true),那子类之中较窄的变量也允许插入父类变量的空隙之中,以节省出一点点空间
- 对齐填充。并不是必然存在的,由于 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全
介绍完了对象的内容,和锁相关的显然就是对象头里存储的那几个内容:
- 其中的重量级锁也就是通常说 synchronized 的对象锁,其中指针指向的是 monitor 对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,monitor 是由ObjectMonitor 实现的,C++实现
- 注意到还有轻量级锁,这是在 jdk6 之后对 synchronized 关键字底层实现的改进
2.3 synchronized 原理
我们已经知道 synchronized 和对象头里的指令有关,也就是我们以前大概的说法:
Java虚拟机可以支持方法级的同步和方法内部一段指令序列(代码块)的同步,这两种同步结构都是使用管程( Monitor,更常见的是直接将它称为“锁”) 来实现的
现在我们讲讲原理
因为对于 synchronized 修饰方法(包括普通和静态方法)、修饰代码块,这两种用法的实现略有不同:
2.4.synchronized 修饰方法
我们测试一个同步方法:
public class Tues {
public static int i ;
public synchronized static void syncTask(){
i++;
}
}
然后反编译 class文件,可以看到:
其中的方法标识:
ACC_PUBLIC
代表public修饰ACC_STATIC
表示是静态方法ACC_SYNCHRONIZED
指明该方法为同步方法。
这个时候我们可以理解《深入理解java虚拟机》里,对于同步方法底层实现的描述如下:
方法级的同步是隐式的。 无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否被声明为同步方法。(静态方法也是如此)
- 当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程(Monitor),然后才能执行方法,最后当方法完成 (无论是正常完成还是非正常完成)时释放管程。
- 在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。
- 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法边界之外时自动释放。
2.5.synchronized修饰代码块
测试一段同步代码:
public class Tues {
public int i;
public void syncTask(){
synchronized (this){
i++;
}
}
}
然后反编译 class 文件:
可以看到,在指令方面多了关于 Monitor 操作的指令,或者和上一种修饰方法的区别来看,是显式的用指令去操作管程(Monitor)了
同理,这个时候我们可以理解《深入理解java虚拟机》里的描述如下:
同步一段指令集序列的情况。Java虚拟机的指令集中有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义。(monitorenter 和 monitorexit 两条指令是 C 语言的实现)正确实现 synchronized 关键字需要 Javac 编译器与 Java 虚拟机两者共同协作支持。Monitor的实现基本都是 C++ 代码,通过JNI(java native interface)的操作,直接和cpu的交互编程
早期synchronized的问题
早期的 synsychronized 的实现就是基于上面所讲的原理,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因
更具体一些的开销,还涉及 java 的线程和操作系统内核线程的关系
前面讲到对象头里存储的内容的时候我们也留了线索,那就是 jdk6 之后多出来轻量级的锁,来改进 synchronized 的实现
我的理解,这个改进就是:从加锁到最后变成以前的那种重量级锁的过程里,新实现出状态不同的锁作为过渡
改进后的锁
偏向锁->自旋锁->轻量级锁->重量级锁。按照这个顺序,锁的重量依次增加
- 偏向锁。他的意思是这个锁会偏向于第一个获得它的线程,当这个线程再次请求锁的时候不需要进行任何同步操作,从而提高性能。那么处于偏向锁模式的时候,对象头的Mark Word 的结构会变为偏向锁结构
研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁的代价而引入偏向锁。那么显然,一旦另一个线程尝试获得这个锁,那么偏向模式就会结束。另一方面,如果程序的大多数锁都是多个线程访问,那么偏向锁就是多余的
public class Example {
private final Object lock = new Object();
public void execute() {
for (int i = 0; i < 1000; i++) {
synchronized (lock) {
// 临界区代码
doSomething();
}
}
}
private void doSomething() {
// 执行某些操作
}
}
在这个例子中,假设方法 execute() 总是由同一个线程调用,那么这个线程在循环中会频繁地获取和释放 lock。在这种情况下,锁总是由同一个线程多次获得
为了优化这种场景下的性能,Java引入了偏向锁(Biased Locking)。偏向锁的机制是,当一个线程第一次获得锁时,锁会“偏向”这个线程,后续该线程再次获取锁时,可以直接进入临界区,而不需要进行复杂的锁竞争操作。这种优化减少了同一线程获取锁的代价,提高了性能
- 轻量级锁。当偏向锁的条件不满足,亦即的确有多线程并发争抢同一锁对象时,但并发数不大时,优先使用轻量级锁。一般只有两个线程争抢锁标记时,优先使用轻量级锁。 此时,对象头的Mark Word 的结构会变为轻量级锁结构
轻量级锁是和传统的重量级锁相比较的,传统的锁使用的是操作系统的互斥量,而轻量级锁是虚拟机基于 CAS 操作进行更新,尝试比较并交换,根据情况决定要不要改为重量级锁。(这个动态过程也就是自旋锁的过程了)
- 重量级锁。重量级锁即为我们在上面探讨的具有完整Monitor功能的锁。
- 自旋锁。自旋锁是一个过渡锁,是从轻量级锁到重量级锁的过渡。也就是CAS
CAS,全称为Compare-And-Swap,是一条CPU的原子指令,其作用是让CPU比较后原子地更新某个位置的值,实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM 只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。
注意:Java中的各种锁对程序员来说是透明的: 在创建锁时,JVM 先创建最轻的锁,若不满足条件则将锁逐次升级.这四种锁之间只能升级,不能降级
上面说的锁都是基于 synchronized 关键字,以及底层的实现涉及到的锁的概念,还有一些别的角度的锁分类:
按照锁的特性分类:
- 悲观锁:独占锁,会导致其他所有需要所的线程都挂起,等待持有所的线程释放锁,就是说它的看法比较悲观,认为悲观锁认为对于同一个数据的并发操作,一定是会发生修改的。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。比如前面讲过的,最传统的 synchronized 修饰的底层实现,或者重量级锁。(但是现在synchronized升级之后,已经不是单纯的悲观锁了)
- 乐观锁:每次不是加锁,而是假设没有冲突而去试探性的完成操作,如果因为冲突失败了就重试,直到成功。比如 CAS 自旋锁的操作,实际上并没有加锁
按照锁的顺序分类:
- 公平锁。公平锁是指多个线程按照申请锁的顺序来获取锁。java 里面可以通过 ReentrantLock 这个锁对象,然后指定是否公平
- 非公平锁。非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。使用 synchronized 是无法指定公平与否的,他是不公平的
独占锁(也叫排他锁)/共享锁:
- 独占锁也叫排他锁,是指该锁一次只能被一个线程所持有。对 ReentrantLock 和 Sychronized 而言都是独占锁
- 共享锁:是指该锁可被多个线程所持有。对 ReentrantReadWriteLock 而言,其读锁是共享锁,其写锁是独占锁。读锁的共享性可保证并发读是非常高效的,读写、写读、写写的过程都是互斥的
独占锁/共享锁是一种广义的说法,互斥锁/读写锁是java里具体的实现
标签:Java,synchronized,对象,虚拟机,线程,方法,轻量级 From: https://www.cnblogs.com/zhangyf1121/p/18287391