最近在看关于线程锁的算法,做一个整理。资料出自于《The Art of Multiprocessor Programming》,算是一个读书笔记吧。示范代码基于java。
第一章是Craig, Landin, and Hagersten (CLH) locks。先上CLHLock的实现逻辑:
Java代码
- public class CLHLock implements Lock {
- private final AtomicReference tail;
- private final ThreadLocal myPred;
- private final ThreadLocal myNode;
- public CLHLock() {
- tail = new AtomicReference(new QNode());
- myNode = new ThreadLocal() {
- protected QNode initialValue() {
- return new QNode();
- }
- };
- myPred = new ThreadLocal();
- }
- @Override
- public void lock() {
- QNode node = myNode.get();
- node.locked = true;
- QNode pred = tail.getAndSet(node);
- myPred.set(pred);
- while (pred.locked) {
- }
- }
- @Override
- public void unlock() {
- QNode node = myNode.get();
- node.locked = false;
- myNode.set(myPred.get());
- }
- private static class QNode {
- volatile boolean locked;
- }
- }
CLH Lock是一个自旋锁,能确保无饥饿性,提供先来先服务的公平性。
锁本身被表示成QNode的虚拟链表。共享的tail保存申请的最后的一个申请lock的线程的myNode域。每个申请lock的线程通过一个本地线程变量pred指向它的前驱。另外一个本地线程变量myNode的locked保存本身的状态,true:正在申请锁或已申请到锁;false:已释放锁。每个线程通过监视自身的前驱状态,自旋等待锁被释放。释放锁时,把自身myNode的locked设为false,使后继线程获的锁,并回收前驱的QNode,等待下次使用,因为自身的QNode可能被tail获后继引用,而前驱不再使用老的QNode。
该算法也是空间有效的,如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是O(L+n),n个线程有n个myNode,L个锁有L个tail。这种算法有一个缺点是在NUMA系统架构下性能表现很差,因为它自旋的locked是前驱线程的,如果内存位置较远,那么性能会受到损失。但是在SMP这种cache一致性的系统架构上表现良好。