Java SE 1.6对synchronized进行了优化,为了减少获得锁和释放锁的性能消耗,引入了偏向锁和轻量级锁。
synchronized的基本语法
synchronized有三种方式加锁:
1.修饰实例方法。作用于当前实例对象加锁
2.修饰静态方法。作用于当前类对象加锁
3.修饰代码块。需指定加锁的对象。
那么锁是如何存储的?
对象在内存中的布局
在Hotspot虚拟机中,对象在内存中的存储布局可以分为3个区域:对象头、实例数据、对象填充。
Mark word
记录了对象和锁相关的信息。当某个对象被synchronized 关键字当成同步锁时,那么围绕这个锁的一系列操作都和 Mark word 有关系。Mark Word 在 32 位虚拟机的长度是 32bit、在 64 位虚拟机的长度是 64bit。Mark Word里面存储的数据会随着锁标志位的变化而变化。
锁的四种状态
锁存在4种状态,分别为无锁、偏向锁、轻量级锁、重量级锁。根据竞争的激烈程度,锁的状态不断地升级。
偏向锁
在大部分的情况下,锁总是由一个线程获得,为了让线程获得锁的代价更低,引入了偏向锁。
当一个线程访问同步代码块时,会使用CAS操作在对象头中存储该线程ID,后续该线程进入和退出同步锁的代码块时,不需要再次加锁和释放锁,而是直接比较对象头中是否存储了指向当前线程的偏向锁。
线程只有初次进入临界区需要执行CAS操作,以后再进入都不会有同步操作开销。
在获取偏向锁的过程中,发现 cas 失败也就是存在线程竞争时,直接把被偏向的锁对象升级到被加了轻量级锁的状态。
轻量级锁
假设只有thread1在临界区中执行,在它执行过程中,thread2也尝试进入临界区,此时会升级到轻量级锁。thread2通过自旋尝试获得锁。
轻量级锁在加锁过程中,用到了自旋锁。自旋锁指当有另外一个线程来竞争锁时,会在原地循环等待,而不是把该线程阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。注意,锁在原地循环的时候,是会消耗 cpu 的。
所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短的时间就能够获得锁了。但是自旋必须要有一定的条件控制,否则如果一个线程执
行同步代码块的时间很长,那么这个线程不断的循环反而会消耗 CPU 资源。默认情况下自旋的次数是 10 次。
在 JDK1.6 之后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
重量级锁
等待重量级锁的线程需要阻塞等待。
每一个 JAVA 对象都会与一个监视器 monitor 关联,我们可以把它理解成为一把锁。当一个线程想要执行同步代码块时,该线程得先获取到 synchronized 修饰的对象对应的 monitor。
monitorenter 表示去获得一个对象监视器。monitorexit 表示释放 monitor 监视器的所有权,使得其他被阻塞的线程可以尝试去获得这个监视器。
monitor 依赖操作系统的 MutexLock(互斥锁)来实现的, 线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。