TAS采用原子操作更新共享状态,同时添加while循环,保证在无法获得锁的同时,可以重复尝试获取锁(实现自旋),而不是挂起线程。如果使用java的话,则可以使用compareAndSet原子操作。
以下是java的TAS版本:
import java.lang.reflect.Field;
import sun.misc.Unsafe;
public class TAS {
private int state = 0;
public static long offset = 0;
public static Unsafe unsafe = null;
public void lock(){
while(!unsafe.compareAndSwapInt(this, offset, 0, 1));
}
public void unlock(){
unsafe.compareAndSwapInt(this, offset, 1, 0);
}
static {
unsafe = getUnsafe();
try {
offset = unsafe.objectFieldOffset(TAS.class.getDeclaredField("state"));
} catch (NoSuchFieldException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private static Unsafe getUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
}
测试程序:
public class Test {
private int a = 0;
public TAS tas = new TAS();
public void increment(){
tas.lock();
a++;
tas.unlock();
System.out.println(a);
}
public static void main(String[] args) {
final Test test = new Test();
for(int i = 0; i < 10000; i++){
new Thread(new Runnable() {
@Override
public void run() {
test.increment();
}
}).start();;
}
}
}
根据多次运行的结果可以看到,锁正常运行。
TTAS的锁实现和TAS的实现相类似。来看一看TAS和TTAS的性能对比,
总的来看,TAS的性能非常糟糕,虽然TTAS相比于TAS性能要好一些,但是和理想值的对比还是差了很多。同时两个曲线都比较陡,也就是说随着线程数的增加,锁的性能越来越差。那么什么导致了锁的性能这么差呢?
上面的图给出了理由:
Test&Set()导致了总线上频繁的广播,用于更新各个线程的内存缓存。因此当持有锁的那个线程释放锁的时候,由于总线的繁忙,而导致了延迟。
为了解决这个问题,可以采用Exponential Backoff
所谓backoff就是当线程无法获取锁的时候,进行休眠一定的时间。这个就在无限休眠和自旋等待之间获得了一个平衡。
来看看改进之前和改进之后的性能差别,
可以看到改进之后,明显地改善了性能。当然这么做也是优缺点的,也就是你必须小心的选择休眠的时间。否则会得不偿失。
总体性能并不是TAS和TTAS最大的问题,其最大的问题是可能会导致starve(饥渴)的出现。由于采用while(自旋锁)实现,所以当某个线程释放锁的时候,其他锁获取这个锁的概率是相同的,但是在最坏情况下,某个线程很早就请求锁,但是每次其他线程释放锁它都无法获取到锁,这就使得这个线程的执行时间非常长,导致了饥渴的出现。
使用基于排队的自旋锁就可以避免线程的饥渴。接下来的文章会介绍。