Anderson’s Algorithm基于队列实现,也就是基于数组的算法。
算法实现:
1)定义共享数组变量flag,大小为N,也就是线程的个数。初始化数组为{true,false,false,......,false}
2)定义共享变量tail,用于记录当前获取锁的线程。
3)每个线程都有一个记录当前槽的临时变量slot。
4)lock: 获取tail变量存储到临时变量slot中,然后将tail++。利用while进行自旋,自旋的条件是slot % n对应的槽为false。
5)unlock:更新slot % n对应的槽为false,更新 (slot + 1)% n的槽为true。
假设某个线程A获取锁,获取锁之前tail = i,获取锁之后tail + 1,假设之后另外一个线程B请求锁,这时tail = i + 1,由于(i + 1) % n处为false,因此这个线程自旋。
此时,获取A释放锁,然后设置了 (i + 1) % n处为true,则线程B退出自旋状态,获取锁。依照这样的方式,之后的第i + 1,i + 2,.....线程依次获取锁。
虽然这个算法并没有实际的队列结构,但是通过数组以及标志位的移动模拟了队列,这样就实现了基于队列的自旋锁。
这样做的好处是不会产生饥渴的现象,先入队列的线程会先获取锁。
实现上面的功能需要注意的是:
1)在lock中定义的局部变量,如何在unlock中继续使用。因为实现的锁和线程不应该有很高的内聚,所以建议使用一个hashmap保存这些变量,key值采用线程的id。
map.put(Thread.currentThread().getId(), slot);
2)flag数组,如果采用正常的boolean或者int,会占用较大的内存。建议采用bit数组以及位操作进行模拟。
来看看这个算法和TTAS的性能对比,
来看看其缺点是什么,
上面图的意识是,thread0~thread3需要获取的flag[0]~flag[3]在同一缓存行内,当thread2修改flag[2]为false的时候,系统会标记其处于的缓存行(cache line)失效,也就是说当其他几个线程想获取其对应的flag的时候会发现缓存行失效,强制从内存中读取新的值,即使这个值并没有被更改。我们知道如果L1,L2,L3级缓存和内存的读取效率依次降低,这就影响了性能。
解决方法是,进行填充恰好使得每个线程需要的flag不在同一个缓存行内(cache line)。
另外一个缺点就是,比较消耗空间。