Java并发编程(五)synchronized关键字

一、synchronized底层原理

1、Java对象头与Monitor

JVM中对象在堆内存中的数据分为三部分,对象头、实例数据和对齐填充。其中,对象头的内容包括Mark Word和类元数据,而Mark Word默认情况下存储着对象的HashCode、分代年龄、锁位标记等信息。每个对象都存在一个monitor与之关联,在对象头中有存储的指针指向monitor,当一个monitor被某个线程持有后,它便处于锁定状态,这就是为什么任意对象都可作为锁。

2、monitor锁定原理

monitor由ObjectMonitor实现,ObjectMonitor有两个队列:_EntryList和_WaitSet,每个等待(获得锁)的线程都是一个ObjectWaiter对象,存放在这两个队列中。

当多个线程同时访问一个对象中,首先进入_EntryList,当有线程获得monitor会进入_Owner区域(如下图),monitor的计时器count+1,若调用wait方法,将释放当前持有的monitor,count-1,同时该线程进入_WaitSet等待被唤醒。

3、synchronized代码块底层原理

同步语句块的实现使用的是monitorenter 和 monitorexit 指令,monitorenter指向同步代码块的开始,执行到moniterenter后,线程就会获得monitor的持有权;而monitorexit指向同步代码块的结束,编译器会确保无论方法正常执行还是发生异常,都会执行到monitorexit。从线程角度看,可以认为monitorenter和monitorexit之间的代码块是原子的。

4、synchronized方法底层原理

方法级的同步是隐式地实现在方法调用和返回操作中,JVM从方法常量池中的方法表结构中ACC_SYNCHRONIZED字段得知一个方法是否是同步方法。方法调用时,调用指令将会检查ACC_SYNCHRONIZED访问标志是否已被设置,如果设置了,执行线程将先会持有monitor。在方法结束或异常时,释放monitor。

二、JVM对sychronized的优化

由于monitor是依赖于底层的操作系统的Mutex Lock来实现的,在操作系统实现线程之间的切换时需要从用户态转换到核心态,所以早期的synchronized效率较低。Java 6对锁进行了优化,之后锁的状态分为4种:无锁状态、偏向锁、轻量级锁、重量级锁。锁的状态可以升级,但只能是单向的,从无锁到重量级锁。

1、偏向锁

如果程序运行过程中,同步锁只有一个线程访问,那么会给线程加偏向锁。如果遇到其他线程抢占锁,则持有偏向锁的线程会挂起,JVM消除该线程的偏向锁,将偏向锁升级为轻量级锁。

偏向锁的情况下线程进入和退出同步块不需要CAS进行加锁和解锁,只需要简单地确定对象头的mark word中是否存储着指向当前线程的指针,因此效率较高。

适用场景:只有一个线程访问同步块。

2、轻量级锁

轻量锁情况下,线程会CAS尝试获取锁,如果出现了两个线程争夺同一个锁,那么就会膨胀成重量级锁。由于轻量级锁情况下竞争的线程不会阻塞,因此效率比重量级锁高,但CAS自旋会消耗CPU资源,因此效率较偏向锁低。

适用场景:线程不存在竞争,追求响应时间,同步块执行速度较快。

3、重量级锁

重量级锁不自旋,但线程会阻塞,响应时间缓慢。

适用场景:追求吞吐量,同步块执行速度较慢。

三、synchronized特性

可重入:synchronized是可重入的,同一线程再次尝试获得锁是允许的。Lock可以判断是否已获得锁,synchronized不行。

不可中断:中断的概念是,当一个线程处于被阻塞状态或者试图执行一个阻塞操作时,使用Thread.interrupt()方式中断该线程,此时将会抛出一个InterruptedException的异常,同时中断状态将会被复位(由中断状态改为非中断状态)。非阻塞状态调用interrupt()并不会导致中断状态重置。

非公平:synchronized是非公平锁,也就是说后请求的线程可以直接抢占资源。

 

参考文档:

https://blog.csdn.net/javazejian/article/details/72828483

猜你喜欢

转载自blog.csdn.net/ss1300460973/article/details/84727907