自旋锁和自适应锁:
在互斥同步对性能最大的影响就是阻塞和唤醒线程的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),在代码中的体现就是不断循环,去判断锁是否释放了,等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。但是线程自旋是需要消耗cup的,说白了就是让cup在做无用功,线程不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。自旋等待本身虽然避免了线程切换的开销,但它是需要占据处理器时间的,如果锁被线程占用的时间很短,自旋等待的效果就会非常好。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
jdk1.6里面加入自适应锁,就是说自旋的时间不再固定,由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能会成功,进而允许这次自旋等待持续相对更长的时间。相反,如果对于某个锁对象,自旋很少获得成功,那个以后在获取这个锁对象时可能省略掉自旋过程,避免浪费cpu资源。
偏向锁:
大多数情况下,锁不仅不存在多线程竞争,而且总是由一个线程多次获得,为了让线程获得锁的代价更低引入偏向锁。偏向锁就是在无竞争的情况下把整个同步都消除掉了,连cas操作都不做了。
具体的操作就是锁对象第一次被线程获取的时候,虚拟机会把对象头里面的标志位设为01,即为偏向模式,同时CAS操作把获得锁的线程id记录在对象的Mark Word之中,之后这个线程再来获取这个对象锁的时候,就判断一下,不用做其他的同步操作了。
1. 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁
2. 每次进入退出同步块都需要CAS更新对象头 3. 争夺轻量级锁失败时,自旋尝试抢占锁可以看到轻量锁适合在短竞争情况下使用,其自旋锁可以保证响应速度快,但自旋操作会占用CPU,所以一些计算时间长的操作不适合使用轻量级锁。
具体的操作就是在线程的帧栈里面建立一个锁记录的空间,用于存储当前对象的Mark Word,CAS操作将对象的Mark Word更新为指向锁记录的指针,同时对象的Mark Word标志位置为00,释放锁的过程就是把锁记录里面的东西更新回对象的Mark Word里面。
重量级锁:
当竞争线程尝试占用轻量级锁失败多次之后,轻量级锁就会膨胀为重量级锁,重量级线程指针指向竞争线程,竞争线程也会阻塞,等待轻量级线程释放锁后唤醒他。
重量级锁的加锁、解锁过程和轻量级锁差不多,区别是:竞争失败后,线程阻塞,释放锁后,唤醒阻塞的线程,不使用自旋锁,不会那么消耗CPU,所以重量级锁适合用在同步块执行时间长的情况下。
对于重量级锁,轻量级锁和偏向锁他们是逐层降低的。
重量级锁是存在实际竞争,锁竞争等待时间较长。
轻量级锁主要针对没有实际竞争,但是可以有多个线程,就是一个执行完了,另外一个再来,这样的情况,或者就是锁竞争等待的时间很短,可以通过自旋的方式来获取锁,这样的情况也比较好。
偏向锁就是针对完全没有实际竞争,并且至始至终就只有一个线程。
参考:《深入理解java虚拟机p400》
《java并发编程的艺术p13》