偏向锁、轻量级锁、重量级锁
在讲述Java的偏向锁之前,先对我在《Java中的锁》一文中提到的自旋锁做一下补充,并且总结一下锁在Java对象中的存储。
自旋锁和自适应自旋
自旋锁的表示为想要持有锁的线程不断的对能够获取锁的判断条件进行判断,直到能够拿到锁。这个过程中不断地自旋将会带来非常大的CPU消耗,所以这种普通的自旋锁的实现一般是有自旋时间或者次数限制,当到达限制的时候还没有拿到锁,那么将会采取方法把线程挂起。
在JDK1.6中,引入了自适应的自旋锁,也就意味着自旋锁的自旋的次数不再是固定不变的,而是由上一个在同一个锁上的自旋时间以及锁的拥有着的状态来决定的:如果在同一个锁 对象上,自旋等待刚刚成功获得了锁,并且持有锁的线程正在运行中,那么虚拟机会认为这个锁这次自旋也有可能成功,进而将允许自旋等待持续相对时间更长;对于某个锁,如果自旋很少成功获得过,那在以后要获取这个锁将可能省略自旋条件,避免浪费处理器资源。
Mark Word
在Java中,synchronized所采用的锁是保存在Java对象头里面的,Java在32位或者64位机器上,对于对象头存储采用的大小是不同的,在32位系统里采用32bit存储,64位系统采用64bit存储。
在32位系统里面,对象头的MarkWord里面有25位存储对象的hashCode,也就是存储对象指向类元数据的指针,有4bit存储对象分代年龄,1bit用来表示是否是偏向锁,2bit来存储锁标志位。
存储内容 | 标志位 | 状态 |
---|---|---|
对象哈希码、对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
空,不需要记录信息 | 11 | GC标记 |
偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
偏向锁
偏向锁是指一个线程持有锁之后,在对象头和栈帧中规定锁记录里存储锁偏向的线程ID,之后该线程退出或者进入这个锁都将不会进行CAS来加锁或者解锁,只需要测试MarkWord里面的标识。如果测试成功,表示线程已经获得了锁,否则测试一下在MarkWord中偏向锁的标识是否设置为1,没有的话采用CAS竞争锁,已经有了的话将对象头的偏向锁指向当前线程。
如果有两个或者多个线程同时竞争锁的时候或者已经有一个线程持有锁另外一个线程申请获得锁,那么这个时候这个锁膨胀为轻量级锁。
偏向锁的目标是消除数据在无竞争情况下的同步源于,将进一步提高程序的并行性能。
轻量级锁
轻量级锁是存在多个线程竞争同一把偏向锁的时候,偏向锁膨胀形成的锁。
在不存在竞争的时候,轻量级锁使用CAS操作进行加锁,因为不使用信号量,所以相对于重量级锁减少了开销,但是当存在竞争的时候,轻量级锁不在有效,将膨胀为重量级锁,等待获取锁的进程将会被阻塞。
在锁竞争的情况下,轻量级锁是可以先通过自旋的方法来获取锁,如果多次获取不成功在将锁膨胀为重量级锁,通过这种方法来减少一定的开销。
重量级锁
重量级锁就是通常意义上的互斥锁,重量锁通过将访问竞态条件的其他线程阻塞,来实现在并发的条件下的安全性。