2020-02-02 2.2.2 条件锁 Condition

本文是Java线程安全和并发编程知识总结的一部分。

2.2.2 条件锁Conditioin

从实际上来看,条件锁本身其实并不是锁,而是从锁上获取到的一个工具包,这个工具包在底层通过条件线程队列,将一组线程和某个条件绑定起来,并提供了适当的时候挂起当前线程、唤醒(条件线程队列内匹配的)其他线程、或被其(条件线程队列内)他线程唤醒的方法。
Condition本身表现为一个接口,以及JDK的内置实现ConditionObject,并提供如下方法:

  • await(): 该方法将导致当前线程挂起,释放锁,直到被其他线程唤醒。
  • signal(): 该方法将唤醒等候当前条件的线程队列中的某个线程。
  • signalAll(): 该方法将唤醒等候当前条件的线程队列中的所有线程。
  • await的各种重载方法或类似方法:awaitUninterruptibly、awaitNanos、awaitUntil

条件对象和条件线程队列的关联是隐式的,当你使用Condition上的上述方法时,就启用了对应的条件线程队列,我们无需直接去创建、维护和使用条件线程队列。同时每个条件对象都对应了一个条件线程队列。实际上,JDK提供的两种条件对象的实现ConditionObject,本身就是两种条件队列实现类 AbstractQueuedSynchronizerAbstractQueuedLongSynchronizer的内部类。

实际上,正如每个对象背后都有一个内置锁一样,每个对象背后,都有一个内置条件线程队列。当你使用Object提供的如下方法时,你就激活了这个队列:

  • wait(): 挂起当前线程直到被唤醒。
  • notify(): 唤醒等候当前对象内置锁的内置条件队列中的某个线程。
  • notifyAll():唤醒等候当前对象内置锁的内置条件队列中的所有线程。

看看是不是和Condition的对应方法非常像?差别只在于内置条件线程队列是和对象的内置锁绑定的,也就是内置锁只有一个内置条件线程队列;而通过Condition访问的条件线程队列是和创建Condition的锁绑定给的,因此一个锁可以存着多个条件线程队列,只要通过。

内置锁、内置条件线程队列,他们已经被默认提供并在你使用synchronized关键词时被使用,非常的方便。但也失去了一些灵活性。比如,你不能将内置锁和任意条件关联。而通过Condition以及其背后的条件线程队列,则允许你自己控制锁到底和那个(或那几个)条件关联。

如何使用条件锁

条件对象和那个状态相关联,是由调用者决定的。下面通过一个例子来说明如何使用。假设我们要实现一个连接池的,获取连接的方法满足如下行为:

  1. 当池中有空闲连接时,获取线程方法立即返回并成功;
  2. 当池中无空闲连接且连接池未满,则获取连接的线程阻塞直到有空闲连接;
  3. 当池已满,且无空闲连接,则获取连接的线程阻塞直到超时或有空闲连接;
  4. 为了简化问题,其他方法就不提供实现了,示意即可;
/**
 * @author xx
 * 2020年2月5日 下午5:28:18 xx
 */
public class ConnectionPool<T> {

    private final Lock lock = new ReentrantLock();
    
    /**
     * 连接池的容量
     */
    private int capacity;
    
    /**
     * 获取连接的超时时长
     */
    private int fetchTimeOut;
    
    /**
     * 当前连接池的总大小
     */
    private int totalSize;
    
    /**
     * 代表连接池尚未满的条件(连接数未超过池大小,和状态属性 totalSize 和 capacity 有关)
     */
    private final Condition isFull = this.lock.newCondition();
    
    /**
     * 代表连接池未满但无空闲连接的条件。
     */
    private final Condition needIncr = this.lock.newCondition();
    
    /**
     * 空闲连接的容器
     */
    private LinkedList<T> freeConnections;
    
    /**
     * 正在使用的连接的容器
     */
    private LinkedList<T> busyConnections;
    
    /**
     * 
     * 构造函数
     * @param capacity 连接池大小
     * @param fetchTimeOut 连接池获取连接超时时长(s)
     */
    public ConnectionPool(int capacity, int fetchTimeOut) {
        this.capacity = capacity;
        this.fetchTimeOut = fetchTimeOut;
        this.totalSize = 0;
        this.freeConnections = new LinkedList<T>();
        this.busyConnections = new LinkedList<T>();
    }
    
    /**
     * 缓存的连接低于1个时,自动增加空闲连接。
     * 2020年2月5日 下午6:40:36 xx添加此方法
     */
    public void incNewConnections() {
        this.lock.lock();
        try {
            // 如果空闲连接数多余1个,则当前线程休眠,并放入 needIncr 条件对应的条件线程队列。
            while (this.freeConnections.size()  > 1) {
                // 该语句将导致当前线程休眠,并被放入 needIncr 条件对应的条件线程队列
                this.needIncr.await();
            }

            // 新缓存一批空闲连接
            this.createConnections();
            
            // 该语句会唤醒 needIncr 条件对应的条件线程队列中所有线程(获取连接的线程),方便通知获取连接的线程可以获取连接了
            this.needIncr.notifyAll();
        } catch (InterruptedException e) {
            throw new RuntimeException("创建连接时线程中断异常", e);
        } finally {
            this.lock.unlock();
        }
    }
    
    /**
     * 从连接池获取一个连接
     * 2020年2月5日 下午5:28:18 xx添加此方法
     */
    public T fetchConnectioin(int index) {
        this.lock.lock();
        try {
            // 如果连接池满了,则当前线程休眠,等到有人释放连接或超时
            while (this.totalSize == this.capacity) {
                // 该语句将导致当前线程休眠并被放入 isFull 条件对应的条件线程队列
                this.isFull.await(this.fetchTimeOut, TimeUnit.SECONDS);
            }
            
            // 如果没有空闲连接,则当前线程挂起等待连接初始化完成
            if (this.freeConnections.isEmpty()) {
                // 该语句将导致当前线程休眠并被放入 needIncr 条件对应的条件线程队列,等待连接创建线程完成工作后唤醒。
                this.needIncr.await();
            }
            
            T element = this.freeConnections.remove(this.freeConnections.size() - 1);
            this.busyConnections.add(element);
            
            return element;
        } catch (InterruptedException e) {
            throw new RuntimeException("获取连接时发生线程中断", e);
        } finally {
            this.lock.unlock();
        }
    }
    
    /**
     * 创建一个物理连接。
     * 本方法无需线程安全保护,因为它会被其他线程安全的方法调用。
     * 2020年2月5日 下午5:28:18 xx添加此方法
     * @param batchSize 一批次大小
     * @return
     */
    private void createConnections() {
        // 如果空闲连接数少于等于1个,则新创建最多10个连接并缓存起来
        int batchSize = this.capacity - this.totalSize;
        if (batchSize > 10) {
            batchSize =  10;
        }

        // 这里模拟创建连接的逻辑
        List<T> ts = new ArrayList<>(10);
        
        this.freeConnections.addAll(ts);
        this.totalSize += batchSize;
    }

    /**
     * 将连接释放会连接池
     * 2020年2月5日 下午5:28:18 xx添加此方法
     */
    public void releaseConnection(T connection) {
        this.lock.lock();
        try {
            if (this.busyConnections.remove(connection)) {
                this.freeConnections.add(connection);
                
                // 该语句将唤醒 isFull 条件对应条件线程队列中的所有线程(都是在等待空闲连接的线程)
                this.isFull.signalAll();
            }
        } finally {
            this.lock.unlock();
        }
    }
}

温馨提示:
本连接池例子只是为了说明如何使用条件锁,真实的连接池绝非如此简单,请勿模仿。
这个例子想要说明如下几点:

  1. 条件是通过锁创建的。
  • 锁创建条件,那个锁创建的条件,就和那个锁绑定。
  • 一个锁可以创建多个条件。
  • 内置锁有且只有一个对应的内置条件线程队列。
  1. 条件锁中的条件(本例中的 isFull 属性),实际上是代表一种状态的抽象概念;至于这种状态具体是什么,有什么意义,都由调用者的逻辑决定;如果调用者的逻辑使用不当,JDK的条件锁本身,并不能分辨,也不能控制。
  • 本例中的 isFull 条件,实际上是和 this.totalSize == this.capacity 这个场景关联的。
  • 本例中的 needIncr 条件,实际上是和 this.freeConnections.size() <= 1 这个场景对应的。
  • 如果两个条件使用的场景发生错乱或遗留,JDK无法发现,将可能导致线程泄露、死锁或其他锁使用不当问题。
  1. 条件本身,实际上只是用来和背后的条件线程队列沟通的工具包;每个条件都有对应的一个条件线程队列。
  • 每个条件都有自己的条件线程队列;
  • 通过await,将当前线程休眠,放到和条件对应的条件线程队列中,并释放锁;但它并不能影响到其他条件对应的条件线程队列。
  • 通过 signalAll/signal,唤醒条件对应的条件线程队列中的所有线程,让它们去竞争锁;但它并不能影响到其他条件对应的条件线程队列。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345