jdk1.6之前,synchronized还是一个重量级锁。
jdk1.6加上了偏向锁和轻量级锁。之后锁就有了4种状态:【无锁】【偏向锁】【轻量级锁】【重量级锁】
在JVM中synchronized重量级锁的底层原理是monitorenter和moniterexit字节码依赖底层操作系统的Mutex Lock来实现的,由于使用Mutex Lock需要将当前线程挂起,并从用户带切换到内核态来执行,这种切换代价是很昂贵的。
研究表明,线程持有锁的时间是比较短暂的,也就是说,当前线程即使现在获取锁失败,但也可能很快就能获取到锁,这种情况将线程挂起很不划算,使用偏向锁或轻量级锁能减低用户态和内核态之间的切换,提高获得锁和释放锁的效率。
在Java虚拟机中,普通对象在内存中分为三块区域:【对象头】、【实例数据】、【对齐填充】,对象头包括markword(8字节)和类型指针(开启压缩指针4字节,不开启8字节,如果是32g以上内存,都是8字节),实例数据就是对象的成员变量,padding就是为了保证对象的大小为8字节的倍数,将对象所占字节数补到能被8整除。数组对象比普通对象在对象头位置多一个数组长度。
存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
偏向锁位、锁标志位的值为:0、 01,此时对象是没有做任何同步限制
偏向锁位、锁标志位的值为:1 01。
大部分场景都不会发生锁资源竞争,并且锁资源往往都是由一个线程获得的。如果这种情况下,同一个线程获取这个锁都需要进行一系列操作,比如说CAS自旋,那这个操作很明显是多余的。
核心思想就是:一个线程获取到了锁,那么锁就会进入偏向模式,当同一个线程再次请求该锁的时候,无需做任何同步,直接进行同步区域执行。这样就省去了大量有关锁申请的操作。
偏向锁加锁过程:
偏向锁撤销的过程:
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
偏向锁的适用场景
始终只有一个线程在执行同步块,一旦有了竞争就升级为轻量级锁,升级过程会多做很多额外操作,尤其是撤销偏向锁的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用。
所以一般JVM并不是一开始就开启偏向锁的,而是有一定的延迟,这也就是为什么会有无锁态的原因。可以使用-XX:BiasedLockingStartupDelay=0来关闭偏向锁的启动延迟, 也可以使用-XX:-UseBiasedLocking=false来关闭偏向锁。
锁标识位(00),CAS自旋把锁对象Mark Word中的线程ID设置为自己。
自适应自旋锁
JDK 1.6引入了更加聪明的自旋锁,叫做自适应自旋锁。他的自旋次数是会变的,线程如果上次自旋成功了,那么这次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么这次自旋也很有可能会再次成功。反之,如果某个锁很少有自旋成功,那么以后的自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
轻量级锁的加锁过程:
轻量级锁的释放
在释放锁的时候如果它发现在它持有锁的期间有其他线程来尝试获取锁了,自旋一定次数后还获取不到锁,则升级为重量级锁,则切换到重量锁。
锁标志位(10),重量锁,对象头中还会存在一个监视器对象,也就是Monitor对象。这个Monitor对象就是实现synchronized的一个关键。
Monitor对象4个重要的属性
锁的获取过程
synchronized是可重入锁,那么它是如何实现可重入的呢?
偏向锁:检查markWord中的线程ID是否是当前线程,如果是的话就获取锁,继续执行代码;
轻量级锁:检查markWord中指向lockRecord的指针是否是指向当前线程的lockRecord,是的话继续执行代码;
重量级锁:检查_owner属性,如果该属性指向了本线程,_count属性+1,并继续执行代码。
就是在一段程序里你用了锁,但是jvm检测到这段程序里不存在共享数据竞争问题,比如使用了线程安全的api,如StringBuffer、Vector、HashTable等,这个时候会隐形的加锁,jvm就会把这个锁消除掉。
比如下面的代码,没有修改共享变量,jvm就会把这个锁消除掉。
public StringBuffer getSb(){
StringBuffer sb= new StringBuffer();
for(int i = 0 ; i < 10 ; i++){
sb.append(i);
}
return sb;
}
不需要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可以让锁尽快释放。
物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。它的思想也是用空间来换时间(如ConcurrentHashMap、LinkedBlockingQueue、LongAdder)。
假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都加锁解锁,效率是非常差的。
ReentrantReadWriteLock 是一个读写锁,读操作加读锁,可以并发读,写操作使用写锁,只能单线程写。