自旋队列锁

1、自旋锁简介

     自旋锁是为保护共享资源而提出一种锁机制。自旋锁与互斥锁比较类似,都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

     自旋锁比较适用于锁使用者保持锁时间比较短的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共享资源的访问时间非常短,自旋锁也可以。但是如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或SMP(多处理器)的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。另外格外注意一点:自旋锁不能递归使用
         cache一致性流量是指所有线程都在同一个共享存储单元上旋转,当执行一些原子操作的时候会导致其他线程该变量的cache副本失效,所以其他线程发生一次cache缺失,重新读值所引起的总线流量。

2、ArrayLock

public class ArrayLock implements Lock {

    private AtomicBoolean[] flag;
    private int size;
    private AtomicInteger tail = new AtomicInteger(0);

    ThreadLocal<Integer> mIdx = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public ArrayLock(int size) {
        if (size <= 0) {
            throw new IllegalArgumentException("size <= 0");
        }
        flag = new AtomicBoolean[size];
        for (int i = 0; i < size; i++) {
            flag[i] = new AtomicBoolean(false);
        }
        this.size = size;
        flag[0].set(true);
    }

    @Override
    public void lock() {
        int idx = tail.getAndIncrement() % size;
        mIdx.set(idx);
        while (!flag[idx].compareAndSet(true, false)) {
        }
    }

    @Override
    public void unlock() {
        int idx = mIdx.get();
        flag[(idx + 1)%size].set(true);
        mIdx.set(0);
    }

}

     一个简单的环形数组锁,当ArrayLock初始化的时候,会初始化一个size大小的AtomicBoolean数组,为了防止过多的线程在一个存储单元上自旋,导致增加cache一致性流量。当lock的时候会给当前节点分配一个下标,在下标%size对应位置的AtomicBoolean上自旋,如果有上一个节点释放锁后,cas操作成功则可以进入临界区。unlock的时候,会将flag中下个AtomicBoolean设置为true,通知下个节点可以进入临界区了。多个节点可能对应flag中的一个slot,在这个slot被设置为true的时候,多个节点开始抢占,抢占成功的就可以进入临界区。如果多个一个slot上对应多个节点还是会增加cache一致性流量,所以还是要预测size的大小,保证每个slot上对应的节点数尽可能的小,而又不浪费flag中的空间。


3、CLHLoc

        CLHLock是一个自旋锁。CLHLock有一个隐式的链表,链表中的节点都只有指向前驱的指针,而没有指向后继的指针。通过对该链表中最后一个元素进行CAS操作来保证并发访问的安全。
public class CLHLock implements Lock {

    public static final int FREE = 1;
    public static final int WAITING = 1 << 1;
    public static final int RELEASED = 1 << 2;

    public AtomicReference<Node> tail = new AtomicReference<Node>();

    ThreadLocal<Node> threadLocal = new ThreadLocal<Node>() {
        @Override
        protected Node initialValue() {
            Node n = new Node();
            return n;
        }
    };

    public void lock() {
        Node cur = threadLocal.get();
        cur.state = WAITING;
        Node prev = tail.getAndSet(cur);
        if (prev != null) {
            cur.prev = prev;
            while (prev.state == WAITING) {
            }
        }
    }

    public void unlock() {
        Node cur = threadLocal.get();
        cur.state = RELEASED;
    }

    static class Node {
        Node prev;
        volatile int state = FREE;
    }
}


    代码中定义了一个原子变量tail,tail可以保证并发访问的时候能够安全的替换tail并且得到当前tail所指向节点的引用prev,将prev设置为当前节点cur的前驱,并且在prev的state上自旋,如果prev的state不为WAITING,那么就解除自旋,执行临界区的代码。释放锁的操作也比较简单,将当前节点的state设置为RELEASED即可。
CLHLock的优点:
1、空间复杂度低,O(L+n),L个锁,n个节点。
2、是公平锁。
3、每个线程在不同的存储单元上自旋,减少了cache一致性流量。
CLHLock的缺点:
在NUMA架构下性能会很差,因为不同的存储单元有可能内存位置较远。

4、MCSLock

public class MCSLock implements Lock {

    public static final int FREE = 1;
    public static final int WAITING = 1 << 1;
    public static final int SINGAL = 1 << 2;

    public AtomicReference<Node> tail = new AtomicReference<Node>();

    ThreadLocal<Node> mNode = new ThreadLocal<Node>() {
        @Override
        protected Node initialValue() {
            Node n = new Node();
            return n;
        }
    };

    public void lock() {
        Node cur = mNode.get();
        Node prev = tail.getAndSet(cur);
        if (prev != null) {
            cur.state = WAITING;
            prev.next = cur;
            while (cur.state == WAITING) {
            }
        }
    }

    public void unlock() {
        Node cur = mNode.get();
        Node next = cur.next;
        if (next == null) {
            if (tail.compareAndSet(cur, null)) {
                return;
            }
            while (cur.next == null) {}
        }
        next.state = SINGAL;
        cur.state = FREE;
        cur.next = null;
    }

    static class Node {
        Node next;
        volatile int state  = FREE;
    }
}
        MCSLock和CLHLock的区别在于,MCSLock的链表是显式的,每个节点都有next指针指向下一个节点。在获得锁的时候首先会先得到该节点的前驱节点,如果为空,则直接进入临界区。非空则将节点插入等待队列尾部,并且在当前节点的state上自旋。解锁的时候,会先判断当前节点的后继是否为空,如果为空则尝试将tail设置为空,成功则证明MCSLock的等待队列为空,可以直接退出。如果设置tail失败,则证明在这段时间内又有新的节点加入等待队列当中,继续执行下面的操作。接下来就是等待队列中有节点的情况,获得当前节点的后继节点,设置其state为SINGAL,通知其停止自旋,进入临界区执行代码。回收节点。
MCSLock的优点:
1、每个节点都在自己的存储单元上自旋,所以在NUMA架构上表现的也非常好。
2、空间复杂度和CLHLock一样。
MCSLock的缺点:
调用CAS、读、写操作比CLHLock多。

转载请标明出处: http://blog.csdn.net/hahaha1232
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值