自定义锁中的同步队列与等待队列

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_34985719/article/details/78009704

在JAVA SE5之后,JAVA中增加了Lock接口用来实现自定义锁对象,但是当进行锁对象的自定义时,一般要在类内部内置一个内部类,而这个内部类需要实现队列同步器AbstractQueuedSynchronizer,并且重写其中的方法,当调用自定义锁的时候,其实是关联到了内部的队列同步器的方法中,从而实现了自定义对象的锁功能。今天和大家分享一些队列同步器内部维护的两个队列(同步队列以及等待队列)的一些学习知识,希望大家多多指教,鉴于博主能力有限,知识有限,对于其中的错误和不当,也希望大家不吝赐教。
首先先明确一点,这篇博客的所有叙述都是基于公平锁的实现来进行阐述,在文章后面会有一些对于非公平锁的分节,但是在此之前都是基于公平锁的,之所以强调这点,是因为对于公平锁而言,等待队列和同步队列都是一个FIFO队列,但是对于非公平锁来说,同步队列无法保证FIFO特性。
先来说明一下同步队列和等待队列的作用。其实从名字上我们知道,队列肯定是用来存储对象的,那这两个队列存储的又是什么对象呢,其实,这两个队列存储的是线程节点对象,那大家可能要问,既然凑是存储线程节点,那么为什么还要分成两个队列来进行存储呢,这就要从同步队列和等待队列的职责上说起了。
第一个我们先来说同步队列,首先我们知道,一个锁的获取是具有排他性的(当然例如读取锁这种共享锁不在讨论范围之内),那么当多个线程来竞争锁的时候,就需要对这些线程来进行排队,怎么来排队呢,当然就是采取队列的方式了。简而言之,同步队列存储的是竞争锁的线程节点,注意,这些竞争锁的线程是在同时活动的,每一个线程都是在不断的自旋检查获取锁的条件,来尝试着获取锁。至于获取条件是什么,下面的文章会讲到。
而等待队列存储的也是线程节点,但是这些线程节点和同步队列中的线程节点并不一样,这些线程节点指向正在等待被唤醒的线程,可以简单理解为等待队列中的线程节点指向的都是正在沉眠的线程。这里需要特别说明一点,等待队列是一个单向队列,但是其头节点和尾节点指针由一个Condition对象进行维护,而Condition由同步器进行维护,而同步队列是一个双向队列,其头节点和尾节点由同步器直接进行维护,如下图:
这里写图片描述
那么两个队列既然都是内置在同步器当中,两个队列又是怎么协同工作的呢,而且队列的出队以及入队又是怎么操作的呢?我们下面继续来和大家分享。
首先,如果我们谈起常见的锁操作,其中一定会有等待操作和唤醒操作,拿普通的对象来说的话,就是Object中的wait()方法和notify()方法,或者是notifyAll()方法,执行了该方法的线程将进行休眠或者唤醒。其实在一个队列同步器中会内置一个同步队列以及一个或者多个等待队列,当一个线程执行了wait方法之后,该线程会释放自己掌握的锁对象,并且将自己封装成一个Node节点加入到等待队列的尾节点之后,也就是说这个新的节点将会成为新的尾节点,而condition的使用如下所示

Lock lock = new ReentrantLock(true);
    Condition condition = lock.newCondition();

    public void testWait(){
        lock.lock();

        try {
            condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void testNotify(){
        lock.lock();
        condition.signal();
        lock.unlock();
    }

Condition是Lock的一个内部类,通过condition来进行等待以及通知操作,当执行

condition.await();

时就会进行上面我们说的将节点加到了等待队列的尾部的操作。那当执行

condition.signal();

来进行通知操作的时候会发生什么呢?如下图所示:
这里写图片描述
当通知操作进行之后,等待队列中的首节点就会出队,然后将自己封装成为一个同步队列的节点,并将自己加入到同步队列尾部,参与到对象的获取过程中去。
那我们知道在Object对象中存在notifyAll方法,用来通知所有等待锁释放的线程,那么Condition中也有相应的方法:

 condition.signalAll();

在执行该方法之后,将会将等待队列中的所有节点全部封装成为同步队列的节点并添加到同步队列中。
简而言之,等待队列就像队列名字一样,存储的是所有在condition上进行等待的线程Node,当线程被唤醒后,线程并不是马上就得到对象的使用权,而是进入到同步队列中进行排队来使用锁。
那么线程是怎么获取锁的呢,下面我们来锁同步队列的工作方式。
首先同步队列是一个双向的FIFO队列,FIFO特性决定了只有队列头节点才是能够出队的节点,而当节点出队时意味着节点指向的线程已经获得了锁的使用权,那么是怎么获取锁对象的呢?
当一个节点指向的线程尝试获取锁的时候,同步队列的线程节点会自旋进行检查,检查过程是在同时进行判断和获取,判断的条件是当前的节点的上一个节点是否存在,当存在时意味着自己并不是头节点,没有满足出队的条件,自然就不能获取锁,当该节点之前已经没有节点了,就会去尝试获取锁,只有获取锁成功时,才能出队。大家可能会有疑惑,不是头节点就可以获取锁吗,为什么还会要判断是否获取锁成功??这个问题主要是因为这样的,当头节点获取到锁并出队时,线程可能在出队后还持有锁,并未释放,但是出队后,这个节点就不再是头节点了,当前队列的头节点因为不能确定一定可以获取锁,所以就需要加判断了。
只有上述两个条件都满足时,线程节点出队,然后获取到锁,并进行操作。简单来说的话,同步队列就是保证了获取锁的公平性,尝试获取锁所花费时间越长的节点就越先出队。
上面的阐述是基于公平锁的,那么对于非公平锁来说呢,其实对于非公平锁,只是少了一个判断条件,就是判断是否有前驱节点的条件被省略掉了,也就是说同步队列中的线程节点都在获取锁的使用权,谁能够获取到,就算谁的。这样的话,就会出现一个问题,就是线程的“饥饿”问题,可能一个线程等待和很长时间之后都不能获取到锁,就造成了饥饿,但是我们上面代码中的ReentrantLock默认就是非公平锁,这是因为非公平锁相较公平锁而言,进行上下文切换的次数更少,吞吐量更大,因为当一个线程释放锁之后,如果再一次尝试获取锁,获取的概率要大很多,具体内容,各位看官可以参考《JAVA并发编程的艺术》一书,具体就不和大家讲了,毕竟博主也是要充电的。。。。说白点,我也没怎么搞会,等弄明白这一点后,再和大家分享。
今天就到这里,感谢敢为赏光话费这么多时间,听博主唠叨。

展开阅读全文

没有更多推荐了,返回首页