Synchronized--实现原理及锁的升降级(二)

116 阅读3分钟

Synchronized实现原理

Synchronized经javac编译后,会在同步代码块前后加上monitorenter和monitorexit两个字节码指令。

当多个线程在竞争锁时,会执行monitorenter指令去获取锁,如果这个对象没有被锁定,或者当前线程获取到了锁,就会把锁的计数器加1,执行monitorexit指令锁的计数器就会减1,如果锁计数器的值为0,意味着锁被释放了。其他未获取到锁的线程会被阻塞等待,直到锁对象被持有者释放。

这里锁计数器的作用是保证可重入性,即同一线程可反复进入持有锁的同步块,避免死锁。其中一个典型的例子是在递归函数里使用锁。

Synchronized优化

Synchronized是个重量级的锁,重量级体现在线程的阻塞与唤醒都需要操作系统从用户态到内核态的切换。

怎么理解呢?对于单核CPU来说的话,同一时间只有一个线程在操作,我们看到的多个线程在并发执行,是由于操作系统对CPU进行时间片划分给多个线程使用。所以当线程进行切换,需要保存阻塞线程栈上的运行数据,以便下次无缝拼接,并将唤醒线程的数据放到栈上执行等一系列的操作,需要耗费处理器很多的时间。

所以从JDK5到JDK6进行了锁优化。

自旋锁

由于现在CPU的多核技术,能让两个以上的线程同时并行执行,那么没有竞争到锁的线程不会立即阻塞,而是执行忙等待(自旋),看持有锁的线程是否能很快释放锁。

自旋锁并不会替代阻塞。

自旋锁的优点是如果某个线程持有锁的时间比较短,那么自旋等待的线程就可以很快持有锁,减少了线程切换的开销。缺点是自旋等待的过程会占用处理器的时间,如果锁被占用的时间很长,会良妃处理器资源。

所以自旋锁的等待时间是有一定限度的,自旋超过限定的次数还没获取到锁就会挂起线程。自旋的默认次数是10次,用户可修改参数配置(-XX:PreBlockSpin)。

自适应自旋锁

JDK6引入了自适应的自旋,也就是说自旋的时间不是固定的。

自适应的自旋是针对锁而言,不是针对线程。如果某个锁,上次自旋等待能成功获取锁,那么JVM会认为这二次也很有可能获取到锁,自适应的时间会长一些。如果自旋很少能获取到锁,以后会直接省略自旋过程。

以上说的是重量级锁,及其优化,JDK6又加入了轻量级锁及偏向锁。

轻量级锁

轻量级锁的应用场景是多个线程在不同时间获取同一把锁,没有多线程的竞争。

使用CAS操作更新对象的Mark Word为指向当前线程栈帧新建的Lock Record的指针。

偏向锁

偏向锁的应用场景是只有一个线程获取锁,完全没有竞争。

使用CAS判断是否有线程获取锁的线程ID记录在对象的Mark Word中,下次同一线程进入同步块就不需要再进行同步操作(加锁、解锁、更新Mark Word)