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
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;
}
}
1、空间复杂度低,O(L+n),L个锁,n个节点。
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,通知其停止自旋,进入临界区执行代码。回收节点。