TAS 是test and set 的缩写,直白的翻译过来就是比较然后测试。java中的原子类大量使用了TAS操作。通过TAS 我们可以安全并且无阻塞的设置原子变量,不用加锁也能进行线程安全的操作。本文目的不是谈原子变量的使用和实现原理的,这个以后会单独来讲。我们主要来看如何使用TAS操作来实现互斥锁。
首先让我们来看看最简单的一种实现TASLock,废话少说直接上代码:
//test and set lock
public class TASLock {
private AtomicBoolean state = new AtomicBoolean(false);
// 加锁
public void lock() {
while (state.getAndSet(true)) {
}
}
// 解锁
public void unlock() {
state.set(false);
}
}
这应该是互斥锁的最简单的实现了吧,加锁和解锁只需要一行有效代码。让我们详细来分析一下TASLock类。TASLock有一个AtomicBoolean类型的字段state,我们用这个字段来标示锁的状态,初始值为false。state为true说明锁已经被某个线程占有,fase则说明锁空闲。AtomicBoolean是布尔值的一个原子类型实现类。关于原子类型的原理和使用场景以后单独写一篇来讲,不是这篇的主要目的。目前只要知道原子类型的方法是线程安全的就ok了。我们再来看一下代码中调用state的两个方法:
(1)getAndSet ,这个方法是设置state的值为参数值,并返回state之前的旧值。
(2)set,这个看方法名就知道了。
这两个方法都是线程安全的,所以不需要同步。通过下面流程图,加锁的原理一目了然。例如有a、b两个线程。a线程先执行lock,此时获取的state是false,所以a线程会直接退出while循环并设置state值true。b线程开始执行lock,此时b线程获取的state值是true,(b也会设置state值为true,不过并没有影响)。所以b会不停的执行getAndSet操作(被阻塞在临界区之外了)。如果此时a执行unlock,则设置state为false,b检测到state为false,会设置state为true并退出while。so easy!
OK,让我们再来看看一种锁TTASLock。依然是先上代码:
/**
*
* Test test and set lock
*
*/
public class TTASLock {
private AtomicBoolean state = new AtomicBoolean(false);
// 加锁
public void lock() {
while (true) {
while (state.get()) {
// 自旋
}
if (!state.getAndSet(true)) {
break;
}
}
}
// 解锁
public void unlock() {
state.set(false);
}
}
TTASlock比TASLock加锁要稍微复杂一下,解锁则是一模一样的。让我们分析一下TTASLock的加锁:
执行lock时,先进入无限循环,然后判断state的值是否为true(此时没有再执行set哦)。如果为true,则一直的循环判断(自旋)。如果state为false,跳出循环并执行getAndSet方法(这一步总是会设置state为true)。如果state原先值为false,跳出无限循环,成功获得锁。否则再次回到判断起点。看一下流程图会更清晰一点:
TTASLock解锁和TALS解锁一样,就是将state设置成false。
两个锁的实现都很简单,逻辑并不是很复杂。这两个锁的主要区别在于加锁上。TASLock和TTASLock加锁从逻辑上来讲,原理是一样的,但是效率上去完全不一样。TTASLock的性能要比TASLock的性能要好的多。这是为什么呢?
这就用上了上一篇讲的那一点点硬件知识。我们先来看一下TTASLock和TASLock加锁上的不同:TTASLock是在get上自旋,一旦符合要求在去执行getandset方法,而TASLock直接在getandset上自旋。让我们用一个例子来说明问题。
TTASLock:有a和b两个线程。a线程执行lock。在执行state.get的时候,第一次是从主存中读取值,然后a将该值放入自己的本地缓存中。a线程发现读取的state值为false,于是跳出循环继续执行state.getAndSet方法。首先a从本地缓存中读取state,发现为false,然后a将state设置为true。一旦state被设置,此时所有线程本地缓存中保存的state值都被置为无效。a成功获得锁。b线程开始执行,b线程先执行get方法,第一次依然是从主存中加载state值并存入本地缓存。b读取的state为true(a设置的),b不停的执行get方法。在a线程改变state之前,b每次读取都是从本地缓存中读取state值(本地旋转),这个过程中是不会消耗总线资源的。a执行解锁操作,设置state为false,所有线程本地缓存中的state值被设置无效。接下来其他线程读取state时都会产生一个cache缺失(不命中),所以b在a执行完unlock后,下一次的get操作又是从主存中读取state值。
TASLock :自旋测试(getandset)每次都会去设置state值,导致其他线程的本地缓存无效。每次都会产生缓存缺失。大量的缓存缺失又会导致总线资源被严重的占用(每次都得从主存中加载值)。更坏的结果本该执行解锁的线程因为总线资源的阻塞而导致被延迟释放锁。所以TASLock性能比TTASLock要低的多。
当然TTASLock也不是完美的加锁方案。因为TTASLock在解锁的时候,会导致其他线程本地缓存中state值无效。所有的线程再次执行get时都会产生缓存缺失,都会从主存中去重新加载,这会导致一阵的总线风暴。虽然产生的流量比TASLock要小,但是在高并发的环境中还是非常可观的。这就需要我们后面要将的如果用队列去取解决缓存的一致性流量问题。
来源:oschina
链接:https://my.oschina.net/u/578710/blog/140479