一 Test-and-Set Lock
所谓测试设置是最基本的锁,每个线程共用一把锁,进入临界区之前看没有有线程在临界区,如果没有,则进入,并上锁,如果有则等待。java实践中利用了原子的设置state变量来保证一次只有一个线程可以获得到锁。
public class TASLock implements Lock { AtomicBoolean state = new AtomicBoolean(false); @Override public void lock() { while (state.getAndSet(true)) { } } @Override public void unlock() { state.set(false); } }
这种锁优点就是简单,缺点是在硬件层面上读取state时候,如果在cache中命中,那么直接从cache中读取就行。但是没有命中,那么将在总线产生一个广播,如果在其他处理器中的cache中找到该地址,那么就以该地址的值做为响应,并且广播该值。更糟糕的是每一个进入自旋的线程都会产生cache缺失,这样产生大量广播流量,延迟较长。
二 指数后退lock
TASLock如果产生大量自旋的线程,则效率很低,避免这个问题就是给后进入自旋的线程一个延迟避让。避让策略有很多种,这里选择指数回退。
class Backoff { int max; int min; int limit; public Backoff(int min, int max, int limit) { this.max = max; this.min = min; this.limit = limit; } public void backoff() { int delay = new Random().nextInt(limit); limit = Math.min(max, 2 * limit); try { Thread.sleep(delay); } catch (InterruptedException e) { e.printStackTrace(); } } } public class BackoffLock implements Lock{ private AtomicBoolean state = new AtomicBoolean(false); @Override public void lock() { Backoff backoff = new Backoff(1000, 5000, 100); while (true) { while (state.get()) {}; if (!state.getAndSet(true)) { return; } else { backoff.backoff(); } } } @Override public void unlock() { state.set(false); } }
三 基于数组的简单队列锁
上面基于TAS的lock并没有真正解决cache缺失的流量问题,所以利用java的ThreadLocal为每一个线程存储一个本地标识索引对应于是否进入临界区而不是在共享的变量上自旋,这样cache流量问题就能得到解决。
public class Alock implements Lock { ThreadLocal<Integer> mysolitindex = new ThreadLocal<Integer>() { protected Integer initvalue() { return 0; } }; AtomicInteger tail; boolean[] flag; int size; Alock (int capacity) { size = capacity; tail = new AtomicInteger(0); flag = new boolean[capacity]; flag[0] = true; } @Override public void lock() { int solt = tail.getAndIncrement() % size; mysolitindex.set(solt); while (!flag[solt]) {}; } }
四 改进的Alock--CLH队列锁
ALock缺点在于并发线程数量固定,空间开销比较大,每次必须分配固定数量的本地线程变量和共享变量。CLHlock解决了空间问题,它利用threadlocal保持2个指针指向pre和current来实现一个隐式的链表,并且通过pre使得cache命中率提高。
class CLHLock implements Lock { AtomicReference<Qnode> tail; ThreadLocal<Qnode> myNode = new Qnode(); public void lock() { Qnode pred = tail.getAndSet(myNode); while (pred.locked) {} }} public void unlock() { myNode.locked.set(false); myNode = pred; } }
class Qnode { AtomicBoolean locked = new AtomicBoolean(true); }
本地线程中保持这指向qnode的指针mynode。如果有L个锁,n个线程,并且每个线程最多同时访问一个锁,那么需要O(L+n)空间。