自旋锁
原理:自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。
适用:锁保护的临界区很小的情况。
实现:
import java.util.concurrent.atomic.AtomicReference;
public class SpinLock {
private final AtomicReference<Thread> owner = new AtomicReference<>();
public void lock() {
Thread currentThread = Thread.currentThread();
// 如果锁被占用了会一直重试
while (!owner.compareAndSet(null, currentThread)) {
}
}
public void unlock() {
Thread currentThread = Thread.currentThread();
// 只有持有锁的线程才会成功
owner.compareAndSet(currentThread, null);
}
}
缺点:
1、CAS操作需要硬件的配合。
2、保证各个CPU的缓存(L1、L2、L3、跨CPU Socket、主存)一致性,通讯开销很大,在多处理器系统上更严重。
3、没法保证公平性。
排队自旋锁
原理:解决公平性问题,类似银行排队:锁拥有一个服务号,表示正在服务的线程,还有一个排队号;每个线程尝试获取锁之前先拿一个排队号,然后不断轮询锁的当前服务号是否是自己的排队号。当线程释放锁时,将服务号加1,这样下一个线程看到这个变化,就退出自旋。
实现:
import java.util.concurrent.atomic.AtomicInteger;
public class TicketLock {
private final AtomicInteger serviceNum = new AtomicInteger(); // 表示当前服务号
private final AtomicInteger ticketNum = new AtomicInteger(); // 用于分发排队号
private final ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>(); // 用于线程保存自己的号码
public void lock() {
// 拿号
int myTicketNum = ticketNum.getAndAdd(1);
// 排队
while (serviceNum.get() != myTicketNum) {
}
// 存号
threadLocal.set(myTicketNum);
}
public void unlock() {
// 拿出我的号码
Integer myTicketNum = threadLocal.get();
if (myTicketNum == null) {
throw new IllegalStateException("call unlock() before lock()");
}
// 更新服务号
serviceNum.compareAndSet(myTicketNum, myTicketNum + 1);
}
// 例子
public static void main(String[] args) {
TicketLock lock = new TicketLock();
Thread t1 = new Thread(() -> {
lock.lock();
try {
System.out.println("t1 get lock");
Thread.sleep(1000);
System.out.println("t1 free lock");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
});
Thread t2 = new Thread(() -> {
lock.lock();
try {
System.out.println("t2 get lock");
Thread.sleep(1000);
System.out.println("t2 free lock");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
});
t1.start();
t2.start();
}
}
缺点:
1、CAS操作需要硬件的配合。
2、每个进程/线程占用的处理器都在读写同一个变量serviceNum,缓存一致性的开销很大。
MCS锁
原理:基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,直接前驱负责通知其结束自旋,减少缓存同步的开销。
实现:
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class MCSLock {
public static class MCSNode {
volatile MCSNode next;
volatile boolean isBlock = true; // 默认是在等待锁
}
volatile MCSNode queue;// 指向最后一个申请锁的MCSNode
private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater
.newUpdater(MCSLock.class, MCSNode.class, "queue");
private void lock(MCSNode currentThread) {
// 获取前驱节点
MCSNode predecessor = UPDATER.getAndSet(this, currentThread);// step 1
if (predecessor != null) { // 等待前驱改变我的block状态
predecessor.next = currentThread;// step 2
while (currentThread.isBlock) {// step 3
}
} else { // 只有一个线程在使用锁,没有前驱来通知它,所以得自己标记自己为非阻塞
currentThread.isBlock = false;
}
}
private void unlock(MCSNode currentThread) {
if (currentThread.isBlock) {// 锁拥有者进行释放锁才有意义
return;
}
if (currentThread.next == null) {// 如果自己是最后一个节点,则清理
if (UPDATER.compareAndSet(this, currentThread, null)) {// step 4
// compareAndSet返回true表示确实没有人排在自己后面
return;
} else {
// 突然有人排在自己后面了,可能还不知道是谁,下面是等待后续者
// 这里之所以要忙等是因为:step 1执行完后,step 2可能还没执行完
while (currentThread.next == null) { // step 5
}
}
}
currentThread.next.isBlock = false;
currentThread.next = null;// for GC
}
// 封装
private final ThreadLocal<MCSNode> threadLocal = new InheritableThreadLocal<>();
public void lock() {
MCSNode node = new MCSNode();
lock(node);
threadLocal.set(node);
}
public void unlock() {
MCSNode node = threadLocal.get();
if (node == null) {
throw new IllegalStateException("call unlock() before lock()");
}
unlock(node);
}
// 例子
public static void main(String[] args) {
MCSLock lock = new MCSLock();
Thread t1 = new Thread(() -> {
lock.lock();
lock.lock();
try {
System.out.println("t1 get lock");
Thread.sleep(1000);
System.out.println("t1 free lock");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
});
Thread t2 = new Thread(() -> {
lock.lock();
try {
System.out.println("t2 get lock");
Thread.sleep(1000);
System.out.println("t2 free lock");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
});
t1.start();
t2.start();
}
}
CLH锁
原理:基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。
实现:
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class CLHLock {
public static class CLHNode {
private volatile boolean isLocked = true; // 默认是在等待锁
}
private volatile CLHNode tail;
private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class, CLHNode.class, "tail");
private void lock(CLHNode currentThread) {
// 获取前驱节点
CLHNode preNode = UPDATER.getAndSet(this, currentThread);
// (1)已有线程占用了锁,进入自旋
if (preNode != null) {
while (preNode.isLocked) {
}
}
// (2)没有线程占用锁,直接返回
}
private void unlock(CLHNode currentThread) {
// 如果队列里只有当前线程,则释放对当前线程的引用(for GC)
if (!UPDATER.compareAndSet(this, currentThread, null)) {
// (1)还有后续线程
currentThread.isLocked = false;// 改变状态,让后续线程结束自旋
}
// (2)没有后续线程,直接返回
}
// 封装
private final ThreadLocal<CLHNode> threadLocal = new InheritableThreadLocal<>();
public void lock() {
CLHNode node = new CLHNode();
lock(node);
threadLocal.set(node);
}
public void unlock() {
CLHNode node = threadLocal.get();
if (node == null) {
throw new IllegalStateException("call unlock() before lock()");
}
unlock(node);
}
public static void main(String[] args) {
CLHLock lock = new CLHLock();
Thread t1 = new Thread(() -> {
lock.lock();
try {
System.out.println("t1 get lock");
Thread.sleep(1000);
System.out.println("t1 free lock");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
});
Thread t2 = new Thread(() -> {
lock.lock();
try {
System.out.println("t2 get lock");
Thread.sleep(1000);
System.out.println("t2 free lock");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
});
t1.start();
t2.start();
}
}
CLH锁 与 MCS锁 的比较
下图是CLH锁和MCS锁队列图示:
差异:
1、从代码实现来看,CLH比MCS要简单得多。
2、从自旋的条件来看,CLH是在前驱节点的属性上自旋,而MCS是在本地属性变量上自旋。
3、从链表队列来看,CLH的队列是隐式的,CLHNode并不实际持有下一个节点;MCS的队列是物理存在的。
4、CLH锁释放时只需要改变自己的属性,MCS锁释放则需要改变后继节点的属性。