Spin Lock -- TAS和TTAS

Spin Lock -- TAS和TTAS_第1张图片

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的性能对比,

Spin Lock -- TAS和TTAS_第2张图片

总的来看,TAS的性能非常糟糕,虽然TTAS相比于TAS性能要好一些,但是和理想值的对比还是差了很多。同时两个曲线都比较陡,也就是说随着线程数的增加,锁的性能越来越差。那么什么导致了锁的性能这么差呢?

上面的图给出了理由:

Test&Set()导致了总线上频繁的广播,用于更新各个线程的内存缓存。因此当持有锁的那个线程释放锁的时候,由于总线的繁忙,而导致了延迟。

为了解决这个问题,可以采用Exponential Backoff

Spin Lock -- TAS和TTAS_第3张图片

所谓backoff就是当线程无法获取锁的时候,进行休眠一定的时间。这个就在无限休眠和自旋等待之间获得了一个平衡。

来看看改进之前和改进之后的性能差别,

Spin Lock -- TAS和TTAS_第4张图片

可以看到改进之后,明显地改善了性能。当然这么做也是优缺点的,也就是你必须小心的选择休眠的时间。否则会得不偿失。


总体性能并不是TAS和TTAS最大的问题,其最大的问题是可能会导致starve(饥渴)的出现。由于采用while(自旋锁)实现,所以当某个线程释放锁的时候,其他锁获取这个锁的概率是相同的,但是在最坏情况下,某个线程很早就请求锁,但是每次其他线程释放锁它都无法获取到锁,这就使得这个线程的执行时间非常长,导致了饥渴的出现。

使用基于排队的自旋锁就可以避免线程的饥渴。接下来的文章会介绍。

你可能感兴趣的:(并发与锁)