并发04--JAVA中的锁

1、Lock接口

Lock与Synchronized实现效果一致,通过获得锁、释放锁等操作来控制多个线程访问共享资源,但是Synchronized将获取锁固话,必须先获得锁,再执行,因此两者对比来说,Synchronized更方便,不需要关注加锁解锁操作;而Lock更灵活,提供了可操作、可中断等特性。

对于Lock接口对比Synchronized的优势如下表格所示:

特性 描述
尝试非阻塞的获取锁 当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取到锁;否则获取锁失败
能被中断的获取锁 与Synchronized不同,获取到锁的线程可以能够响应中断,当获取到锁的线程中断时,中断异常将会被抛出,同时锁被释放
超时获取锁 在指定的截止时间之前获取锁,如果截止时间到了仍然没有获得锁,则返回

Lock是一个接口,它定义了锁获取和锁释放的基本操作,Lock的API如下表所示:

方法名称 描述
void lock() 获取锁,调用该方法的线程会阻塞的获取锁,当获取到锁后,从该方法返回
void lockInterruptibly() throws InterruptedException 可中断的获取锁,和lock()方法的不同在于该方法会响应中断,即在锁的获取种,可以中断该线程
boolean tryLock() 尝试非阻塞的获取锁,调用该方法后立刻返回,如果获取到锁,返回true,否则返回false
boolean tryLock(long time,TimeUnit unit) throws InterruptedException

超时的获取锁,当前线程会有如下三种返回:

1、当前线程在超时时间内获得锁

2、当前线程在超时时间内被中断

3、超时时间结束,返回false

void unlock() 释放锁
Condition newCondition() 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后,当前线程释放锁

2、队列同步器

队列同步器AbstactQueuedSyncronizer是用来构建锁或者其他同步组件的基础,他与锁的关系:

  锁是面向使用者,他定义了使用者与锁交互的接口,隐藏了锁的实现细节;

  同步器面向的是锁的实现者,他简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待和唤醒等底层操作。

锁和同步器很好的隔离了使用者和实现者所需要关注的领域。

2.1、队列同步器的接口和示例

同步器的设计是基于模板方法的,也就是说,使用者需要继承同步类并重写执行的方法,随后将同步器组合在自定义同步组件实现种,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。

同步器可以重写的方法如下表所示:

方法名称 描述
protected boolean tryAcquire(int arg) 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态
protected boolean tryRelease(int arg) 独占式释放锁,等待获取同步状态的线程将有机会获取同步状态
protected int tryAcquireShared(int arg) 共享式获取同步状态,返回大于等于0时,表示获取成功,反之,获取失败
protected boolean tryReleaseShared(int arg) 共享式释放同步锁
protected boolean isHeldExclusively() 当前同步器是否再独占模式下被线程占用,一般该方法表示是否被当前线程独占

实现自定义同步组件时,将会调用同步器提供的模板方法,这些模板方法如下:

方法名称 描述
void acquire(int arg) 独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用重写的tryAcquire(in arg)方法
void acquireInterruptibly(int arg) 与acquire(int arg)方法相同,但是该方法支持响应中断,当前线程未获取到同步状态而进入同步队列种,如果当前线程被中断,该方法会抛出InterruptedException异常并返回
boolean tryAcquireNanos(int arg,longnanos) 在acquireInterruptibily方法上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将会返回false,如果获取到了,则返回true
void acquireShared(int arg) 共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占锁的主要区别时在同一时刻可以由多个线程获取到同步状态
void acquireSharedInterruptibily(int arg) 与acquireShared方法一致,支持相应中断
boolean tryAquireSharedNanos(int arg,long nanos) 在acquireSharedInterruptibily方法上增加了超时设置
boolean release(int arg) 独占式释放同步状态,该方法会在释放同步状态之后,将同步队列种第一个节点包含的线程唤醒
boolean releaseShare(int arg) 共享式的释放同步状态
Collection<Thread> getQueuedThreads() 获取等待在同步队列上的线程集合

由上表可见,主要涉及的操作分为三类:独占锁的获取和释放、共享锁的获取和释放、查询同步队列中等待线程

自定义独占锁代码如下:

public class Mutex implements Lock {

    //自定义同步器
    private static class Sync extends AbstractQueuedSynchronizer{
        //是否处于占用状态
        @Override
        protected boolean isHeldExclusively() { 
            return getState() == 1;
        }

        //方状态为0时获取锁
        @Override
        public boolean tryAcquire(int acquire){
            if(compareAndSetState(0,1)){
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        //释放锁
        @Override
        protected boolean tryRelease(int arg) {
            if(getState() == 0){
                throw new IllegalMonitorStateException(null);
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        //返回一个Condition,每个Condition都包含了一个condition队列
        Condition newCondition(){
            return new ConditionObject();
        }
    }


    private final Sync sync = new Sync();

    @Override
    public void lock() {
        sync.acquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireSharedNanos(1,unit.toNanos(time));
    }

    @Override
    public void unlock() {
        sync.release(1);
    }

    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }

    public boolean isLocked(){
        return sync.isHeldExclusively();
    }

    public boolean hasQueuedThreads(){
        return sync.hasQueuedThreads();
    }
}

  上述示例中,Mutex是一个自定义的同步组件,它在同一时刻只允许一个线程占有锁。Mutex中定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。在tryAcquire方法中,如果经过CAS设置成功为1,则代表获取了同步状态,而在tryRelease方法中只是将同步状态重置为0。用户使用Metux时并不会和内部同步器的实现打交道,而是调用Mutex的lock、unLock等方法。在Mutex中,以获取锁的lock方法为例,只需要在实现方法中调用同步器的模板方法acquire即可,当前线程调用该方法获取同步状态失败后会被加入到等待队列中。

2.2、同步器的实现分析

同步器主要由同步队列、独占式锁的获取和释放、共享式锁的获取和释放这三部分组成

(1)同步队列

   同步队列如下图所示:

 

 

 同步器中存在head节点和tail节点,head节点存的是同步队列中头节点的引用,tail节点存的是同步队列中尾节点的引用。

当一个线程获取同步状态失败时,就会将当前线程的信息组装成为一个Node节点,然后将节点加入同步队列中。

组装成Node节点的线程属性信息如下:

属性类型及名称 描述
int waitStatus

等待状态;包含如下状态:

1、CANCELLED,值为1,由于在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待,节点进入该状态将不会变化。

2、SIGNL,值为-1,后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点得以运行

3、CONDITION,值为-2,节点在等待队列中,节点等待在condition上,当其他线程对condition调用了signl()方法后,该节点将会从等待状态中转移到同步队列中,加入到对同步状态的获取中

4、PROPAGATE,值为-3,表示下一次共享式同步锁获取将会无条件的被传播下去

5、INTIAL,值为0,初始状态

Node prev 前驱节点,当节点接入同步队列时设置(尾部添加)
Node next 后继节点
Node nextWaiter 等待队列中的后继节点,如果当前节点是共享的,那么这个字段是一个SHARED常量,也就是说节点类型(独占或共享)和等待队列中的后继节点共用一个字段
Thread thread 获取同步状态的线程

说了属性后,那么由以上属性生成node节点再加入同步队列时,同步队列如下图所示:

 

 

 可以发现,新的node节点的prev节点设置成了原尾节点,同步器的tail节点被设置为了新增的node节点,原尾节点的next指向了新增的node节点。

对于线程获取到同步状态,出同步队列的情况如下图所示:

 

 

 可以看到,原首节点的next节点变为空,而同步器中的head节点指向了原来头节点的后继节点,同时,原头节点对应后继节点的prev指向也被清空。

(2)独占式同步队列的获取与释放

那么对于独占式获取锁失败时,有可能存在并发插入的问题,那么同步器是是用无限CAS将Node插入的方式保证顺序

独占式同步队列获取锁的流程如下:

 

 

 可以看到:如果一个线程要获取同步状态,如果获取成功,直接返回;如果获取失败,则会生成Node节点,然后将Node节点CAS设置为尾节点;然后该节点同样通过CAS无限循环判断自己的前驱节点是否为头节点,如果是头节点,那么该节点就尝试获取同步状态,获取成功,将该节点设置为头节点,然后返回获取同步状态成功;如果前驱结点不是头节点或尝试获取同步状态失败,线程则进入阻塞状态,除非线程被中断或者前驱结点被释放。

说完了新增一个node节点到同步队列,那么一个node节点是如何出队列呢?

同步器的head节点对应的node所对应的线程,是拥有同步状态的,如果该线程操作完毕,释放同步状态时,通知后继节点自己被释放(对应新增节点中线程中断或前驱节点被释放流程分支),此时后继节点争取获得同步状态。

总体来说,独占式锁的流程为:在获取同步状态时,同步器维护了一个同步队列,获取同步状态失败的线程都会被加入到队列并在队列中进行自旋;移除队列或停止自旋的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态的时候,同步器调用tryRelease方法释放同步状态,然后还行头节点的后继节点。

独占式锁还有一个特殊的方法,就是超时独占锁,其流程如下:

 

 

 对比独式和超时独占式,发现超时独占式获取和释放同步状态中,多了一步对超时时间判断的流程,如果时间超时仍未获取到同步状态,就做返回。

(3)共享式锁的获取与释放

对于共享式锁的获取和释放,基本上与独占式一致,区别就是共享式可以允许多个线程同时获取同步状态,因此在尝试获取同步状态时,调用tryAcquireShared方法时,如果返回值大于0,说明获取到了同步状态(说明还允许有几个线程获取同步状态),同时,在调用releaseShared方法时,需要确保线程安全的释放同步状态,一般是通过循环和CAS来保证的,因为共享式释放同步状态的操作会同时操作多个线程。

那么节点来就可以自定义一个同步组件--TwinsLock,允许同时有两个线程获取同步状态。

package com.example.jdk8demo;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class TwinsLock implements Lock {

    private final Sync sync = new Sync(2);

    private static final class Sync extends AbstractQueuedSynchronizer {
        Sync(int count){
            if(count<=0) {
                throw new IllegalArgumentException("");
            }
            setState(count);
        }

        @Override
        protected int tryAcquireShared(int arg) {
            for (;;){
                int current = getState();
                int newcount = current - arg;
                if(newcount>=0 && compareAndSetState(current, newcount)){
                    return newcount;
                }
            }
        }

        @Override
        protected boolean tryReleaseShared(int arg) {
            for (;;){
                int current = getState();
                int newcount = current + arg;
                if(compareAndSetState(current, newcount)){
                    return true;
                }
            }
        }
    }

    @Override
    public void lock() {

    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return sync.tryAcquireShared(1)>0?true:false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public void unlock() {
        sync.tryReleaseShared(1);
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

测试代码:

package com.example.jdk8demo;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;

@SpringBootTest
@Slf4j
public class TwinsTest {
    @Test
    public void test() throws Exception{
        final Lock lock = new TwinsLock();
        class Worker extends Thread{
            @Override
            public void run() {
                while (true){
                    try {
                        lock.tryLock();
                        TimeUnit.SECONDS.sleep(1);
                        log.info("{}==={}",Thread.currentThread().getName(),LocalDateTime.now());
                        TimeUnit.SECONDS.sleep(1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }finally {
                        lock.unlock();
                    }
                }
            }
        }
        for (int i=0;i<10;i++){
            Worker worker = new Worker();
            worker.setDaemon(true);
            worker.start();
        }

        for (int i=0;i<10;i++){
            TimeUnit.SECONDS.sleep(1);
            log.info("=======================");
        }
    }
}

输出结果:

 

 从结果可见,从来没有多于两个线程获取到同步状态。

3、重入锁

  ReentrantLock,重入锁,顾名思义,就是支持重入的锁,他表示该锁能够支持一个线程对资源重复加锁;除此之外,该锁还支持获取锁时的公平和非公平选择。

  前面代码示例Mutex就是一个非重入锁,因为它可以将自己阻塞。

  那么重入锁是如何实现的呢?

可以看下其源码:

        /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }


        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

  以上是以非公平锁为例的获取同步状态及释放同步状态的源码,可以看到:

  在获取同步状态时,如果现在没有线程获得同步状态,那么直接更新state为当前获取的同步状态数量,同时设置持有同步状态的线程为当前线程;如果当前同步状态已经有线程持有,那么判断该持有线程是否为当前线程,如果是,则可以再次获取锁,同时将获取锁的次数累加。可以发现,冲入锁的获取和非重入锁的唯一区别就是,如果当有线程持有该同步状态时,会判断持有的线程是否为当前线程。

  在释放锁时,同样会判断持有同步状态的线程是否为当前线程,如果是,就会去更新持有同步状态的次数,如果次数为0,说明持有同步状态的线程已经全部释放了同步状态,那么将持有同步状态的线程置为空。

  这里说明一点,synchronized隐式的支持重入。

  上述源代码是以非公平锁的方式处理重入,那么公平锁如何处理呢,源代码如下:

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

  可以发现,公平锁和非公平锁的唯一区别就是,在第一次获取锁时,公平锁会调用hasQueuedPredecessors()方法判断当前节点是否还存在前驱节点(即是否非首节点),如果还存在前驱节点,说明存在比当前线程更早排队的线程,因此不能获取到锁;如果没有前驱节点,说明当前线程已经是等待最久的线程,从而可以获取同步状态。

  接下来,就模拟一个测试,去验证一下公平锁和非公平锁:

   非公平锁可能使线程“饥饿”,但是重入锁的默认实现就是非公平锁,因为非公平锁的开销更小(因为有可能刚释放锁的线程又重新获得锁,减少了线程切换),保证了更大的吞吐量。

4、读写锁

  读写锁,在读的时候允许多个线程并发访问,写的时候只允许一个线程访问。

  当获取读锁时,所有后续的写锁都会被阻塞;当获取写锁时,后续所有的读写操作都会被阻塞。

  JAVA并发包提供的读写锁的实现是ReentrantReadWriteLock,它提供的特性如下:

特性 描述
公平性选择 支持非公平(默认)和公平锁的获取方式,吞吐量还是非公平优于公平锁
重进入 该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后能够再次获取写锁,同时也能够获取读锁
锁降级 遵循获取写锁、获取读锁再释放写锁的次序,写锁能降级成为读锁

  ReadWriteLock进定义了获取读锁和写锁的两个方法readLock()和writeLock(),而其实现ReentrantReadWriteLock,除了这两个接口外,还提供了供外部监控内部工作状态的方法,如下:

方法名称 描述
int getReadLockCount() 返回当前读锁获取的次数(非线程数,因为一个线程可能多次重入)
int getWriteLockCount() 返回当前写锁获取的次数
boolean isWriteLocked() 判断写锁是否被获取
int getWriteHoldCount() 返回当前写锁被获取的线程

示例读写锁使用方式:

package com.example.jdk8demo;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Cache {
    static Map<String,Object> map = new HashMap<>();
    static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    static Lock readLock = reentrantReadWriteLock.readLock();
    static Lock writeLock = reentrantReadWriteLock.writeLock();

    public static final Object get(String key){
        readLock.lock();
        try{
            return map.get(key);
        }finally {
            readLock.unlock();
        }
    }

    public static final void set(String key,Object object){
        writeLock.lock();
        try{
            map.put(key,object);
        }finally {
            writeLock.unlock();
        }
    }
    public static void clear(){
        writeLock.lock();
        try{
            map.clear();
        }finally {
            writeLock.unlock();
        }
    }
}

  ReentrantReadWriteLock的实现主要包含了:读写状态的设计、写锁的获取与释放、读锁的获取与释放、锁降级这四个方面。

  ReentrantReadWriteLock的读写状态仍然是用的是同步器的同步状态,由于同步状态只能表示一个锁的获取次数,而读写锁需要在同步状态上维护多个线程的读和一个线程的写,因此读写锁将同步状态切分为两个部分,高16位表示读,低16位表示写。

 

 

   如上图所示,高16位表示读锁(黄色部分),表示读锁重入了一次(共有两次访问读锁),低16位表示写锁(蓝色部分),重入了2次(共有3次获取写锁)。

  读写锁是使用位运算款苏确定读锁和写锁各自的状态的:写锁的状态(蓝色部分)只需要将高16位去掉即可:S&0x0000FFFF;读锁的状态(黄色部分)将高16位右移16位即可,S>>16

  根据状态的划分推出结论:S不等于0时,当写状态等于0,则说明读状态大于0,即读锁被获取。

  对于读锁的获取,JDK实现如下:

        protected final boolean tryAcquire(int acquires) {
            /*
             * Walkthrough:
             * 1. If read count nonzero or write count nonzero
             *    and owner is a different thread, fail.
             * 2. If count would saturate, fail. (This can only
             *    happen if count is already nonzero.)
             * 3. Otherwise, this thread is eligible for lock if
             *    it is either a reentrant acquire or
             *    queue policy allows it. If so, update state
             *    and set owner.
             */
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

  由代码实现可见,写锁是一个排他锁,如果当前线程已经获取了写锁,则增加写状态;如果当前线程在获取写锁时,读锁已经被获取(该读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。

  写锁的释放与ReentrantLock基本上一致,每次释放均减少写状态,当前状态位0时表示写锁已经被释放,从而等待的读写线程能够继续访问读写锁。

  对于读锁的获取,由于存在一些一些监控的需要,实现有点复杂,就不把源码贴出来了,总体来说,就是如果其他线程已经获得了写锁,则当前线程获取读锁失败;如果当前线程获取了写锁或者写锁未被获取,则通过CAS保证该线程获取读锁。

  上面说的监控需要指的是:主要是新增了一些功能,比如getReadHoldCount(),作用是返回当前线程获取读锁的次数。读状态是所有线程获取读锁的次数总和,而每个线程获取读锁次数只能保存在ThreadLocal中,由线程自身维护,这就使得读锁的实现变得复杂。

  锁降级指的是写锁降级为读锁。如果当前线程拥有写锁,然后将写锁释放,再获取读锁,这种情况不叫锁降级。锁降级是在写锁不释放的同时,获取读锁,然后写操作处理完后释放写锁(读锁仍然持有)的操作才叫锁降级。

  ReentrantReadWriteLock不支持锁升级。

5、LockSupport工具

  LockSupport定义了一组静态变量,这些方法提供了最基本的线程阻塞和线程唤醒功能,而LockSupport也成为构建同步组件的基础工具。

  LockSupport提供的方法如下:

方法 描述
void park() 阻塞当前线程,如果调用unpark()方法或者当前线程中断,才能从park()方法返回
void parkNanos(long nanos) 阻塞当前线程,最长不超过nanos纳秒,返回条件在park()方法上增加了超时返回
void parkUntil(long deadlone) 阻塞当前线程,知道deadline时间(从1970年开始到deadline的毫秒数)
void park(Object blocker) 阻塞当前线程,如果调用unpark()方法或者当前线程中断,才能从park()方法返回
void parkNanos(Object blocker,long nanos) 阻塞当前线程,最长不超过nanos纳秒,返回条件在park()方法上增加了超时返回
void parkUntil(Object blocker,long deadline) 阻塞当前线程,知道deadline时间(从1970年开始到deadline的毫秒数)
void unpark() 唤醒处于阻塞状态的线程thread

  可以发现,参数由Object blocker和没有该参数的一组park方法的描述一摸一样,那么他们的区别到底是什么呢,区别就是如果查看线程dump信息的时候,带有Object blocker的方法可以提供更多的信息。

  在JDK1.5之前,当线程阻塞在一个对象上时(使用synchronized关键字),通过线程dump可以查看到该线程的阻塞对象,方便定位问题,而JDK1.5推出Lock工具时却遗漏了这点,导致的线程dump时无法提供阻塞的信息,因此在JDK1.6中,LockSupport提供了3个带有Object blocker对象的方法,用以替换原有的不带Object blocker的方法,从而提供阻塞对象信息。

6、Condition接口

 任意一个Java对象,都拥有一组监视器对象方法(定义在Object上),主要包括wait()、wait(long timeout)、notify()、notifyAll()方法,这些方法与synchronized关键字配合,可以实现等待通知模式。Condition接口也提供了类似Object的监视器方法,与lock配合可以实现等待/通知模式,但是这两者在使用的时候还是有差别的。

对比项 Object Motior Methods Condition
前置条件 获取对象锁

调用Lock.lock()获取锁

调用Lock.newConditon()获取Condition对象

调用方式 直接调用:如:object.wait() 直接调用:如:condition.await()
等待队列个数 一个 多个
当前线程释放锁并进入等待状态 支持 支持
当前线程释放锁并进入等待状态,在等待状态中不响应中断 不支持 支持
当前线程释放锁并进入超时等待0 支持 支持
当前线程释放锁到将来的某个时刻 不支持 支持
唤醒等待队列中的一个线程 支持 支持
唤醒等待队列中的所有线程 支持 支持

Condition提供的部分方法及描述:

方法 描述
void await() throws InterruptedException

当前线程进入等待直到被通知(signal)或中断,当前线程进入运行状态且从await方法返回,包括:

(1)其他线程调用该Condition的signal()或signalAll()方法,而当前线程被选中唤醒

(2)其他线程中断(调用interrupt()方法)当前线程

(3)如果当前线程从await()方法返回,那么表明该线程已经获取了Condition对象对应的锁

void awaitUninterruptibly() 当前线程进入等待直到被通知,并且对中断不敏感
long awaitNanos(long nanosTimeout) throws InterruptedException 当前线程进入等待状态直到被通知、中断或者超时。返回值表示剩余的时间,如果返回值大于0,表明在超时时间的前x纳秒返回了;如果返回值小于等于0,则是超时
boolean awaitUntil(Date deadline) throws InterruptedException 当前线程进入等待直到被通知、中断或到某个时间,如果没有到指定时间就被通知,方法返回true,否则表示到了指定时间,防范返回false
void signal() 唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关联的锁
void signalAll() 唤醒所有等待在Condition上的线程,能够从等待方法返回的线程必须获得与Condition相关联的锁

  使用Condition实现等待通知机制代码示例:

public class BoundedQueue<T> {
    private Object[] items;

    private int addIndex,removeIndex,count;
    private Lock lock = new ReentrantLock();
    private Condition notEmpty = lock.newCondition();
    private Condition notFull = lock.newCondition();

    public BoundedQueue(int size){
        items = new Object[size];
    }

    //添加一个元素
    public void add(T t) throws InterruptedException{
        lock.lock();
        try{
            //如果队列已经满了,阻塞
            while (count == items.length){
                notFull.await();
            }
            //队列没有满,直接添加
            items[addIndex] = t;
            //如果到了最后一个,则重新从头添加
            if(++addIndex == items.length){
                addIndex = 0;
            }
            //长度累加
            ++count;
            //通知被阻塞的线程
            notEmpty.signal();
        }finally {
            lock.unlock();
        }
    }

    public T remove(T t) throws InterruptedException{
        lock.lock();
        try{
            while (count == 0){
                notEmpty.await();
            }
            Object object = items[removeIndex];
            if(++removeIndex == items.length){
                removeIndex = 0;
            }
            --count;
            notFull.signal();
            return (T)object;
        }finally {
            lock.unlock();
        }
    }
}

 

  以上述代码为例,首先获得锁,目的是确保数组修改的可见性和排他性,当数组内元素数量等于数组长度时,,表示数组已满,调用notFull.await()方法,当前线程释放锁进入等待状态,如果数组长度不等于数组内元素的时候,表示数组未满,则继续向数组内添加元素,同时通知再notEmpty()上等待通知的线程;删除元素类似。

  Condition是同步器AbstractQueuedSychonrized的内部类,每个Condition对象都包含着一个队列(等待队列),因此在并发包的同步器拥有一个同步队列和多个等待队列(Object的监视器模型上,一个对象拥有一个同步队列和一个等待队列),其对应关系如下图所示:

  

 

 如果调用了await()方法,会使当前线程进入等待队列(调用newCondition的等待队列),同时线程状态位等待状态。从队列的角度看,调用了await()方法,相当于从同步队列的首节点移动到了等待队列的尾节点。

  ConditionObject中await()和gignal()方法如下:

        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            long savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
        public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

  如果调用signal()方法,就是从等待队列的头节点移动到同步队列的尾节点,移动后,调用acquireQueued()方法加入到获取同步状态的竞争中。

posted @ 2020-06-15 19:07  李聪龙  阅读(268)  评论(0编辑  收藏  举报