自旋锁VS适应性自旋锁

2018-11-22  本文已影响0人  萝灏

阻塞或者唤醒一个JAVA的线程需要操作系统切换CPU状态来完成,这种状态的转换需要耗费处理器时间。如果同步代码块中的内容过于简单,很可能导致状态转换消耗的时间比用户代码执行的时间还要长。所以在短暂的等待之后就可以继续进行的线程,为了让线程等待一下,需要让线程进行自旋,在自旋完成之后,前面锁定了同步资源的线程已经释放了锁,那么当前线程就可以不需要阻塞便直接获取同步资源,从而避免了线程切换的开销。这就是自旋锁。

自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。

自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。

自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
在自旋锁中 另有三种常见的锁形式:TicketLock、CLHlock和MCSlock

TicketLock

import java.util.concurrent.atomic.AtomicInteger;
public class TicketLock {
    private AtomicInteger                     serviceNum = new AtomicInteger();
    private AtomicInteger                     ticketNum  = new AtomicInteger();
    private static final ThreadLocal<Integer> LOCAL      = new ThreadLocal<Integer>();
    public void lock() {
        int myticket = ticketNum.getAndIncrement();
        LOCAL.set(myticket);
        while (myticket != serviceNum.get()) {
        }
    }
    public void unlock() {
        int myticket = LOCAL.get();
        serviceNum.compareAndSet(myticket, myticket + 1);
    }
}

上边是TicketLock的源码,是如何实现的自旋?当第一个线程获取Lock的时候,myticket是0,但是ticketNum已经变成了1,这个时候myticket和serviceNum的value是相等的,于是继续运行。此时,第二个线程获取的myticket是1,但是serviceNum的value还是0,就会在while上自旋,直到第一个线程准备调用unLock方法,把serviceNum的值修改为第一个线程的myticket加1。由于value是volatile的,所以第二个线程此时跳出了while循环。通过这种方式完成了自旋,避免了加锁导致的阻塞以及线程切换。

上一篇下一篇

猜你喜欢

热点阅读