Synchronized

Synchronized

1、synchronized基本原理

synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现。基于进入与退出Monitor对象,实现方法与代码块同步。监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁,性能较低。当 然,JVM 内置锁在1.5之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,内置锁的并发性能已经基本与Lock持平。

Synchronized关键字,在编译的字节码中,加入了两条指令来进行代码的同步。

Monitor enter:加锁

每个对象有一个监视器锁(monitor),当monitor被占用时就会处于锁定状态,线程执行monitor enter指令时尝试获取monitor的所有权,过程如下:

如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

如果该线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。

如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

Monitor exit:释放

执行monitor exit的线程必须是objectref所对应的monitor的所有者。

指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常。

2、synchronized的优化

2.1、用户态和内核态

①内核态

CPU可以访问内存所有数据,包括外围设备,例如硬盘,网卡。CPU也可以将自己从一个程序切换到另一个程序。

②用户态

只能受限的访问内存,且不允许访问外围设备,占用CPU的能力被剥夺,CPU资源可以被其他程序获取。

之所以会有这样的区分,是为了防止用户进程获取别的程序的内存数据,或者获取外围设备的数据。

Synchronized原本是依赖操作系统实现的,因此在使用synchronized同步锁的时候需要进行用户态到内核态的切换。简单来说,在JVM中monitor enter和monitor exit字节码是依赖于底层操作系统的Mutex Lock来实现,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的。

2.2、 jdk1.6及其之后的优化

对象头:每个对象都拥有对象头,对象头由Mark World ,指向类的指针,以及数组长度三部分组成。锁升级主要依赖Mark Word中的锁标志位和释放偏向锁标识位。

①偏向锁(无锁)

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后(线程的id会记录在对象的Mark Word锁标志位中),消除这个线程锁重入的开销,看起来让这个线程得到了偏护。(第二次还是这个线程进来就不需要重复加锁,基本无开销)。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

②轻量级锁:

轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁(自旋锁)。没有抢到锁的线程将自旋,获取锁的操作。轻量级锁的意图是在没有多线程竞争的情况下,通过CAS操作尝试将Mark Word锁标志位更新为指向LockRecord的指针,减少了使用重量级锁的系统互斥量产生的性能消耗。

长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)

③重量级锁:

如果锁竞争情况严重,某个达到最大自旋次数(10次默认)的线程,会将轻量级锁升级为重量级锁,重量级锁则直接将自己挂起。在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。

虚拟机使用CAS操作尝试将MarkWord更新为指向LockRecord的指针,如果更新成功表示线程就拥有 该对象的锁;如果失败,会检查MarkWord是否指向当前线程的栈帧,如果是,表示当前线程已经拥 有这个锁;如果不是,说明这个锁被其他线程抢占,此时膨胀为重量级锁。

Java 语言中,如果一个变量要被多线程访问,可以使用 volatile 关键字声明它为“易变的”;如果一个变量要被某个线程独享,Java 中可以通过 java.lang.ThreadLocal 类来实现线程本地存储的功能。

上一篇:Linux ALSA 音频系统:逻辑设备篇


下一篇:Nacos源码启动及变更PostgreSQL数据库