自旋锁

自旋锁的定义

自旋锁指的是当线程获取不到资源时,不是进入阻塞状态,而是让当前的线程不停地执行空循环,直到循环条件被其他线程改变,进入临界区。

  • 具体实现如下:
public class SpinLock {
    private AtomicReference sign = new AtomicReference<>();
    
    public void lock(){
        Thread current = Thread.currentThread();
        while (!sign.compareAndSet(null,current)){
            
        }
    }
    
    public void unlock(){
        Thread cur = Thread.currentThread();
        sign.compareAndSet(cur,null);
    }
}

在这里使用了CAS原子操作,lock函数将sign设置为当前线程,并且预测原来的值为空。unlock函数将sign设置为null,并且预测值为当前线程。

当有第二个线程调用lock操作时,由于sign值不为空,导致循环一直被执行,直到第一个线程调用unlock操作将sign设置为null,第二个线程才能进入临界区。

  • 进行测试:
public static void main(String[] args){
    SpinLock lock = new SpinLock();
    for (int i = 0; i < 2; i ++){
        new Thread("Thread-" + i){
            @Override
            public void run() {
                AddClass ac = new AddClass(lock);
                //进行100次累加
                for (int j = 0; j < 100; j ++)
                    try {
                        ac.add();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }.start();
        }
    }

    static class AddClass{
        //设置为静态变量,表示为临界区
        private static int i;
        private SpinLock lock;
        public AddClass(SpinLock lock){
            this.lock = lock;
        }

        //无锁自增
        public void add() throws InterruptedException {
            i ++;
            System.out.println(Thread.currentThread().getName()+": i = " + i);
            //睡眠一段时间,保证线程1,2之间可以相互切换
            Thread.sleep(200);
        }

        //有锁自增
        public void addWithLock() throws InterruptedException {
            lock.lock();
            i ++;
            Thread.sleep(100);
            System.out.println(Thread.currentThread().getName()+": i = " + i);
            lock.unlock();
        }
    }
输出结果:
add: Thread-1: i = 176
addWithlock: Thread-1: i = 200

可见,自旋锁可以保证临界区中的互斥访问。

自旋锁的评价

-优点:由于自旋锁不改变线程的状态,所以线程的运行会比较快。

  • 缺点:当线程数比较多时,会有较多的线程占用CPU空跑,影响其他线程获取CPU资源,导致性能下降。
  • 使用场景:锁竞争不激烈且占用锁的时间较短的场景,如jdk中的Atomic类。
自旋锁的其中种类
  • TicketLock:主要解决访问顺序的问题
public class TicketSpinLock {
    //类比票号
    private AtomicInteger ticketNum = new AtomicInteger(0);
    //类比显示器上的号码
    private AtomicInteger owner = new AtomicInteger(0);

    private static final ThreadLocal mTicketLocal = new ThreadLocal<>();

    public void lock(){
        //注意这里获取到的数为加一前的数
        int mTicket = ticketNum.getAndIncrement();
        mTicketLocal.set(mTicket);
        while (mTicket != owner.get()){

        }
    }

    public void unlock(){
        int mTicket = mTicketLocal.get();
        owner.compareAndSet(mTicket,mTicket + 1);
    }
}

上述过程类比我们去银行办业务,在去取票机上取票后再排队等候,每个人都是一个线程,而票号就是ticketNum,柜台显示器显示的号码就是owner,当票号和显示器上的号码一致时,才可以去办理业务。而mTicketLocal在这里的作用是保存票号。

  • CLHLock:确保无饥饿性,提供先到先服务的公平性。
public class CLHLock {
    //保存最后一个申请lock的线程的mNode域
    AtomicReference tail = new AtomicReference<>();
    //指向其前驱
    ThreadLocal mPred;
    //保存当前的状态
    ThreadLocal mNode;

    public static class QNode{
        public volatile boolean locked = false;
    }

    public CLHLock(){
        mNode = new ThreadLocal(){
            @Override
            protected QNode initialValue() {
                return new QNode();
            }
        };

        mPred = new ThreadLocal();
    }

    public void lock(){
        QNode qnode = mNode.get();
        qnode.locked = true;
        QNode pred = tail.getAndSet(qnode);
        mPred.set(pred);
        while (pred.locked){

        }
    }

    public void unlock(){
        QNode qNode = mNode.get();
        qNode.locked = false;
        mNode.set(mPred.get());
    }
}

锁本身表示成QNode的虚拟链表。共享的tail保存申请的最后的一个申请lock的线程的mNode域,每个申请lock的线程通过一个本地变量pred指向它的前驱。另一个本地线程变量mNode的locked保存本身的状态(true:正在申请锁或已申请到锁;false:已释放锁)。

每个线程通过监视自身的前驱状态,自旋等待锁被释放。释放锁时,把自身的mNode的locked设为false,使后续线程获得锁,并回收前驱的QNode,等待下一次使用。

当一个线程申请锁时:
a. 首先会实例化一个QNode节点,并将其设置为自己的本地线程变量myNode;
b. 然后将myNode的locked域设置为true,表明该线程正在等待获取锁;
c. 接着通过AtomicReference的getAndSet()方法,将myNode设置为队列的最后一个节点,并返回其前驱节点;
d. 最后该线程一直在其前驱节点的locked域自旋,直到locked域变为false,即前驱节点释放了锁。注意,对于SMP架构,由于是在cache中自旋,所以是高效的;

当一个线程释放锁时:
a. 将myNode的locked域设置为false,使得后继线程停止自旋以便获取到锁;
b. 然后重用前驱节点,将前驱节点设置为myNode,以便下一次该线程获取锁时使用。之所以能重用,是因为此时其前驱线程不会再使用该前驱节点了。而该线程原来的myNode,可能被其后继线程或tail引用。对于java语言来说,重用实际上是没有必要的,因为如果不重用,则前驱节点是可以被jvm回收的,从而降低内存的使用。

自旋锁_第1张图片
Paste_Image.png
  • MCS锁

同CLH锁一样,每个申请锁的线程通过一个QNode对象表示,QNode中有一个volatile boolean类型的成员变量locked,locked为true,则表示对应的线程已经获取到了锁或者正在等待锁;locked为false,则表示对应的线程已经释放了锁。
锁则由多个QNode节点组成链表表示,与CLH锁不同的是,MCS中的锁不是虚拟链表,而是实际链表,每个QNode节点都有一个next域指向后继结点。

public class MCSLock {
    public class QNode{
        public volatile boolean locked = false;
        public QNode next = null;
    }

    private AtomicReference tail;
    private ThreadLocal myNode;

    public MCSLock(){
        tail = new AtomicReference<>(null);
        myNode = new ThreadLocal(){
            @Override
            protected QNode initialValue() {
                return new QNode();
            }
        };
    }

    public void lock(){
        QNode qnode = myNode.get();
        QNode pred = tail.getAndSet(qnode);
        if (pred != null){
            qnode.locked = true;
            pred.next = qnode;
        }
        while (qnode.locked){

        }
    }

    public void unlock(){
        QNode qnode = myNode.get();
        if (qnode.next == null){
            if (tail.compareAndSet(qnode,null))
                return;
            while (qnode.next == null){

            }
        }
        qnode.next.locked = false;
        qnode.next = null;
    }
}

当一个线程申请锁时:
a. 首先会实例化一个QNode节点,并将其设置为自己的本地线程变量myNode;
b. 然后通过调用AtomicReference的getAndset()方法,将myNode设置为队列的最后一个节点,并返回其前驱节点。
c. 接着判断前面是否已经有其他线程加入到锁的等待队列中,即前驱节点是否为空。如果前驱节点为空,则说明自己是第一个加入到锁的等待队列中的线程,执行自旋,由于locked域初始值为false,所以能立即退出自旋,获取到锁;
d. 如果前驱节点非空,则首先将myNode的locked域设置为true,表明自己正在等待获取锁;同时将前驱结点的next域指向myNode;
e. 最后该线程一直在自己节点的locked域自旋,直到locked域变为false,即前驱节点释放了锁。

当一个线程释放锁时,会处于以下三种情况之一中:
a. 此刻没有其它线程正在申请锁,即当前节点的next域指向null,且tail指向当前节点:此种情况通过调用AtomicReference的compareAndSet()方法,将tail指向null;然后直接返回。
b. 此刻有其他线程正在申请锁,而且已更新tail域,但还未将前驱节点的next域指向它。即当前节点的next域指向null,且tail不是指向当前节点:此种情况则一直等待其他线程设置前驱节点的next域后,才将后继节点的locked域设置为false,以便后继节点退出自旋,从而获取到释放的锁。
c. 此刻有其他线程正在申请锁,而且已更新tail域,而且已将前驱节点的next域指向它。即当前节点的next域非空:此种情况则直接将后继节点的locked域设置为false,以便后继节点退出自旋,从而获取到释放的锁。

自旋锁_第2张图片
Paste_Image.png

你可能感兴趣的:(自旋锁)