synchronized应用
synchronized有三种方式来加锁,分别是:方法锁,对象锁synchronized(this),类锁synchronized(Demo.Class)。其中在方法锁层面可以有如下3种方式:
- 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
- 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
synchronized括号后面的对象:
synchronized扩号后面的对象是一把锁,在java中任意一个对象都可以成为锁,简单来说,我们把object比喻是一个key,拥有这个key的线程才能执行这个方法,拿到这个key以后在执行方法过程中,这个key是随身携带的,并且只有一把。如果后续的线程想访问当前方法,因为没有key所以不能访问只能在门口等着,等之前的线程把key放回去。所以,synchronized锁定的对象必须是同一个,如果是不同对象,就意味着是不同的房间的钥匙,对于访问者来说是没有任何影响的。
synchronized原理和锁升级过程
锁信息是放在对象头的mark word中的,下图是mark word的数据结构,将围绕着这个图进行分析锁的原理。
Hotspot32位:
锁升级
锁的4中状态(级别从低到高):无锁态 --> 偏向锁 --> 轻量级锁 (无锁,乐观锁,自旋锁,自适应自旋锁) —> 重量级锁。
hotspot的实现为例(64位):
- 分代年龄:对象被GC的次数,4个bit,能表示16个数,最大值是15。CMS垃圾回收器默认是6,其他回收器默认16。
- hashCode:只有调用了hashCode()方法之后,对象的hashCode才会存在这里面,没调用则是空。
- mark word的低三位表示锁状态,其中一位表示偏向锁,另两位表示其他锁。
- lock record:持有displaced word和锁住对象的元数据;解释器使用lock record来检测非法的锁状态;隐式地充当锁重入机制的计数器。
偏向锁
因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,在竞争不激烈的时候,常常是一个线程多次获得同一个锁;因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。
线程A获取锁后,会将mark word的高54bit设置成指向线程A的指针。偏向锁不会主动释放锁,当线程A再获取锁的时候,会检查高54bit的线程指针;如果是指向A的,若A还是存活则直接进入,若A不存活则将锁设置为无锁态。如果高54bit指向的不是A,不是则锁撤销偏向锁状态升级为轻量级锁。
偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用-XX:BiasedLockingStartUpDelay=0;
如果不想要偏向锁,那么可以通过-XX:-UseBiasedLocking = false来设置。
轻量级锁
轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。
当字节码解释器执行monitorenter字节码轻量锁住一个对象时,就会在获取锁的线程的栈上显式或者隐式分配一个lock record,里面记录的锁对象的信息。同时锁对象的高62位指向了获得该锁线程的lock record。多个线程通过CAS来修改锁对象的高62位来获得锁。
CAS有一定的次数限制,来避免自旋过度消耗CPU,比如10次或者cpu超过1/2。如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。
重量级锁
重量级锁需要向操作系统申请资源,会挂起没有获得锁的线程,进入等待队列。等待操作系统的调度,然后再映射回用户空间。
总结
注意:为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。一句话就是锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。
锁消除
Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。
例如:
public void addString(String s1, String s2){
StringBuffer sb = new StringBuffer();
sb.append(s1).append(s2);
}
StringBuffer是线程安全的,因为他的关键方法都是被synchronized修饰的,但是我们看上面的代码会发现:sb这个引用只会在add方法里被使用,不可能被其他线程引用(局部变量,栈私有),因此sb是不可能共享的资源,JVM会自动消除StringBuffer对象内部的锁。
锁粗化
按理来说,同步块的作用范围应该尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗。
锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。
例如:
StringBuffer sb = new StringBuffer();
public String addString(String s1){
for (int i = 0; i < 1000; i++) {
sb.append(s1);
}
return sb.toString();
}
JVM会检测到这样一连串的操作会对同一个对象加锁(while循环内1000次执行append,没有锁粗化的话就要进行100次加锁/解锁),此时JVM就会将加锁的范围粗化到这一连串的操作外部,使得这一连串操作只加一次锁即可。
synchronized底层实现分析
首先了解一下JIT(Just In Time Compiler)计时编译器,这是针对解释性语言而言的,并非虚拟机必须,是一种优化手段。Hotspot虚拟机就是用的这种技术手段。Hotspot虚拟机的执行引擎执行java代码是可采用解释执行和编译执行两种方式。Hotspot中的编译器是javac,他的工作是将源代码编译成字节码。有了字节码,就有解释器来进行解释执行,即将字节码解释成机器码;但是Hotspot会把执行频率较高的方法或语句块通过JIT直接编译成本地机器码,提高了代码执行的效率。
synchronized底层也是用lock comexchg…指令实现的。