聊聊高并发(九)实现几种自旋锁(四)

这篇看一下时限队列锁的一种实现方式。 java并发包中的Lock定义包含了时限锁的接口:

public interface Lock {
 
    void lock();
 
    void lockInterruptibly() throws InterruptedException;
 
    boolean tryLock();
 
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
 
    void unlock();
 
    Condition newCondition();
}

tryLock就是实现锁的接口,它支持限时操作,支持中断操作。这两个特性很重要,可以防止死锁,也可以在死锁的情况下取消锁。

因为这两个特性的需要,队列锁的节点需要支持“退出队列”的机制,也就是说当发生超时或者线程中断的情况下,线程能从队列中出队列,不影响其他节点继续等待。之前实现的几种队列锁都不支持退出机制,一旦发生队列中的线程长时间阻塞,那么后续所有的线程都会被动阻塞。

我们看一种限时队列锁的实现,它有几个要点:

  1. 定义一个共享的AVAILABLE节点,当一个节点的preNode指向AVAILABLE时,表示这个节点获得锁

  2. QNode节点维护一个preNode引用,这个引用只有当获得锁时,会指向AVAILABLE,或者超时了会指向它的前一个节点,其他等待锁的时候都是Null,因为一旦一个节点超时了,需要让它的后续节点指向它的前驱节点,所以只有超时的时候会给preNode设置值(指向AVAILABLE节点除外)。

  3. 使用一个AtomicReference原子变量tail来形成一个虚拟的单向链表结构。tail的getAndSet操作会返回之前的节点的引用,相当于获得了前驱节点。当获得锁后,前驱节点引用就释放了,前驱节点就可以被GC回收

  4. 支持中断操作,Thread.isInterrupted()可以获得线程中断的信息,一旦获取中断信息,就抛出中断异常。需要注意的时,线程中断信息发出时,并不是要求线程马上中断,而是告知了线程要中断的信息,程序自己控制中断的地点。

  5. 由于线程只有一个ThreadLocal的myNode变量指向自己的节点,所以获取锁时,使用了每次new一个新的Node,并设置给线程的方式,避免unlock时对node的操作影响后续节点的状态,也可以使线程多次获得锁。这里可以考虑像CLHLock那样,维护两个ThreadLocal的引用,释放锁时把myNode的引用指向已经不使用的前驱节点,这样避免无谓的new操作。

package com.zc.lock;
 
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
 
/**
 * 时限队列锁,支持tryLock超时操作
 * QNode维护一个指针preNode指向前一个节点。当preNode == AVAILABLE表示已经释放锁。当preNode == null表示等待锁
 * tail维护一个虚拟链表,通过tail.getAndSet方法获得前一个节点,并在前一个节点自旋,当释放锁时前一个节点的preNode == AVAIABLE,自动通知后一个节点获取锁
 * 当一个节点超时或者被中断,那么它的前驱节点不为空。后续节点看到它的前驱节点不为空,并且不是AVAILABLE时,知道这个节点退出了,就会跳过它
 * 当节点获得锁,进入临界区后,它的前驱节点可以被回收
 * **/
public class TimeoutLock implements TryLock{
    // 声明为静态变量,防止被临时回收
    private static final QNode AVAILABLE = new QNode();
    
    // 原子变量指向队尾
    private AtomicReference<QNode> tail;
 
    ThreadLocal<QNode> myNode;
    
    public TimeoutLock(){
        tail = new AtomicReference<QNode>(null);
        myNode = new ThreadLocal<QNode>(){
            protected QNode initialValue(){
                return new QNode();
            }
        };
    }
    
    @Override
    public void lock() {
        // 和CLHLock不同,每次新建一个Node,并设置给线程,目的是支持同一个线程可以多次获得锁,而不影响链中其他节点的状态
        // CLHLock不需要每次新建Node是因为它使用了两个指针,一个指向前驱节点。而前驱节点释放后就可以回收了。
        // CLHLock每次释放锁时设置myNode为失效的前驱节点,也是为了支持同一个线程可以多次获取锁而不影响其他节点
        QNode node = new QNode();
        myNode.set(node);
        QNode pre = tail.getAndSet(node);
        if(pre != null){
            // 在前一个节点自旋,当前一个节点是AVAILABLE时,表示它获得锁
            while(pre.preNode != AVAILABLE){
                
            }
        }
    }
 
    @Override
    public void unlock() {
        QNode node = myNode.get();
        // CAS操作,如果为true,表示是唯一节点,直接释放就行;否则把preNode指向AVAILABLE
        if(!tail.compareAndSet(node, null)){
            node.preNode = AVAILABLE;
        }
    }
    
    @Override
    //TimeUnit只支持毫秒
    public boolean trylock(long time, TimeUnit unit) throws InterruptedException {
        if(Thread.interrupted()){
            throw new InterruptedException();
        }
        boolean isInterrupted = false;
        long startTime = System.currentTimeMillis();
        long duration = TimeUnit.MILLISECONDS.convert(time, unit);
        // 注意:每次tryLock都要new新的Node,为了同一个线程可以多次获得锁。如果每个线程都使用同一个节点,会影响链中其他的节点
        QNode node = new QNode();
        myNode.set(node);
        // 尝试一次获取锁
        QNode pre = tail.getAndSet(node);
        // 第一个节点或者之前的节点都是已经释放了锁的节点, pre==AVAILABLE表示获得了锁
        if(pre == null || pre == AVAILABLE){
            return true;
        }
        // 在给定时间内对preNode自旋
        while((System.currentTimeMillis() - startTime < duration) && !isInterrupted){
            QNode predPreNode = pre.preNode;
            // 表示前一个节点已经释放了锁,设置了preNode域,否则preNode域为空
            if(predPreNode == AVAILABLE){
                return true;
            }
            // 当prePreNode != null时,只有两种情况,就是它超时了,或者被中断了。
            // 跳过prePreNode不为空的节点,继续自旋它的下一个节点
            else if(predPreNode != null){
                pre = predPreNode;
            }
            if(Thread.interrupted()){
                isInterrupted = true;
            }
        }
        
        // 超时或者interrupted,都要设置node的前驱节点不为空
        node.preNode = pre;
        
        if(isInterrupted){
            throw new InterruptedException();
        }
        
        return false;
    }
    
    public static class QNode {
        volatile QNode preNode;
    }
    
    public String toString(){
        return "TimeoutLock";
    }
 
}

TimeoutLock具备所有CLHLock的特性,比如无饥饿,先来先服务的公平性,在多个共享变量上自旋,从而控制合理的缓存一致性流量等等,并且支持了限时操作和中断操作。

使用限时锁时有固定的模板,防止锁被错误使用。

Lock lock = ...;
if (lock.tryLock()) {
     try {
     // manipulate protected state
     } finally {
        lock.unlock();
     }
} else {
    // perform alternative actions
}

同样,我们之前验证锁正确性的测试用例同样对TimeoutLock有效,这里不重复帖代码了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容