多线程高级

多线程高级类

concurrent包的结构层次

在针对并发编程中,Doug Lea大师为我们提供了大量实用,高性能的工具类,针对这些代码进行研究会让我们队并发编程的掌握更加透彻也会大大提升我们队并发编程技术的热爱。这些代码在java.util.concurrent包下。

156818874213433

其中包含了两个子包:atomic以及lock,另外在concurrent下的阻塞队列以及executors,这些就是concurrent包中的精华。而这些类的实现主要是依赖于volatile以及CAS,从整体上来看concurrent包的整体实现图如下图所示:

156818882319043

原子类

java.util.concurrent.atomic原子操作类包里面提供了一组原子变量类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)。可以对基本数据、数组中的基本数据、对类中的基本数据进行操作。原子变量类相当于一种泛化的volatile变量,能够支持原子的和有条件的读-改-写操作。

Java提供了12个原子变量类,可以分为4组:标量类、更新器类、数组类以及复合变量类。

156810328112934

原子变量类分组

java.util.concurrent.atomic中的类可以分成4组:

  • 标量类(Scalar):AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
  • 数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
  • 更新器类:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
  • 复合变量类:AtomicMarkableReference,AtomicStampedReference

第一组,AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference这四种基本类型用来处理布尔,整数,长整数,对象四种数据。其内部实现不是简单的使用synchronized,而是一个更为高效的方式CAS (compare and swap) + volatile和native方法,从而避免了synchronized的高开销,执行效率大为提升。如AtomicInteger的实现片断为:

private static final Unsafe unsafe = Unsafe.getUnsafe();  
private volatile int value;
public final int get() {  
        return value;  
}
public final void set(int newValue) {  
        value = newValue;  
}
public final boolean compareAndSet(int expect, int update) {  
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
}
public final int getAndIncrement(){
        for(;;){
        //先取出AtomicInteger的当前值
        int current=get();
        //对当前值加1操作
        int next=current+1;
        //通过compareAndSet方法比较当前值有没有被其它线程修改过,若修改过返回false则再次进入compareAndSet方法判断
        if(compareAndSet(current,next))
        return current;
        }
}
public final boolean weakCompareAndSet(int expect, int update) {  
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
}
  • 构造函数(两个构造函数)
    默认的构造函数:初始化的数据分别是false,0,0,null
    带参构造函数:参数为初始化的数据
  • set()和get()方法:可以原子地设定和获取atomic的数据。类似于volatile,保证数据会在主存中设置或读取
  • void set()和void lazySet():set设置为给定值,直接修改原始值;lazySet延时设置变量值,这个等价于set()方法,但是由于字段是volatile类型的,因此次字段的修改会比普通字段(非volatile字段)有稍微的性能延时(尽管可以忽略),所以如果不是想立即读取设置的新值,允许在“后台”修改值,那么此方法就很有用。
  • compareAndSet()和weakCompareAndSet()方法:
    这两个方法都是conditional modifier方法。这2个方法接受2个参数,一个是期望数据(expected),一个是新数据(new);如果atomic里面的数据和期望数据一 致,则将新数据设定给atomic的数据,返回true,表明成功;否则就不设定,并返回false。JSR规范中说:以原子方式读取和有条件地写入变量但不创建任何 happen-before 排序,因此不提供与除 weakCompareAndSet 目标外任何变量以前或后续读取或写入操作有关的任何保证。大意就是说调用weakCompareAndSet时并不能保证不存在happen- before的发生(也就是可能存在指令重排序导致此操作失败)。但是从Java源码来看,其实此方法并没有实现JSR规范的要求,最后效果和 compareAndSet是等效的,都调用了unsafe.compareAndSwapInt()完成操作。
  • getAndSet()方法
    原子的将变量设定为新数据,同时返回先前的旧数据,
    其本质是get()操作,然后做set()操作。尽管这2个操作都是atomic,但是他们合并在一起的时候,就不是atomic。在Java的源程序的级别上,如果不依赖synchronized的机制来完成这个工作,是不可能的。只有依靠native方法才可以。

对于 AtomicInteger、AtomicLong还提供了一些特别的方法:

  • getAndIncrement():以原子方式将当前值加 1,相当于线程安全的i++操作.
  • incrementAndGet():以原子方式将当前值加 1, 相当于线程安全的++i操作.
  • getAndDecrement():以原子方式将当前值减 1, 相当于线程安全的i—操作.
  • decrementAndGet():以原子方式将当前值减 1,相当于线程安全的—i操作.
  • addAndGet():以原子方式将给定值与当前值相加, 实际上就是等于线程安全的i =i+delta操作.
  • getAndAdd():以原子方式将给定值与当前值相加, 相当于线程安全的t=i;i+=delta;return t;操作。 以实现一些加法,减法原子操作。(注意 —i、++i不是原子操作,其中包含有3个操作步骤:第一步,读取i;第二步,加1或减1;第三步:写回内存);

java有8个基本类型,而这里只有3个,那么其他5个如果也想原子操作呢?其实可以参考AtomicBoolean的实现,它先将boolean类型值转换成int。另外三组这里不做分析,有兴趣大家可以自己去了解下。

ReentrantLock

前面有提到lock,ReentrantLock实现lock是可重入的轻量级锁。
可重入:线程拿到锁之后,只要没有释放,可以多次进入锁住代码而不需要重新获取锁。
轻量级:是相对synchronized是重量级锁来说的,ReentrantLock底层的实现是通过内部类Sync来实现相关功能的,所以,介绍ReentrantLock将重点介绍其内部类Sync。

ReentrantLock源码

//ReentrantLock实现lock接口
public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    private final Sync sync;//定义内部内
    //内部类继承AbstractQueuedSynchronizer
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;
       //定义lock方法
        abstract void lock();
        //非公平锁尝试加锁
        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;
        }
        protected final boolean isHeldExclusively() {
            // While we must in general read state before owner,
            // we don't need to do so to check if current thread is owner
            return getExclusiveOwnerThread() == Thread.currentThread();
        }
        //condition类,挂起线程组
        final ConditionObject newCondition() {
            return new ConditionObject();
        }
        // Methods relayed from outer class
        final Thread getOwner() {
            return getState() == 0 ? null : getExclusiveOwnerThread();
        }
        final int getHoldCount() {
            return isHeldExclusively() ? getState() : 0;
        }
        final boolean isLocked() {
            return getState() != 0;
        }
        /**
         * Reconstitutes the instance from a stream (that is, deserializes it).
         */
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
    }
    /**
     * 非公平锁
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        final void lock() {
            //一上来就尝试获得锁
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    /**
     * 公平锁
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
        final void lock() {
            acquire(1);
        }
        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;
        }
    }
    //默认非公平锁
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    public void lock() {
        sync.lock();
    }
    //可中断加锁
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
    public void unlock() {
        sync.release(1);
    }
    public Condition newCondition() {
        return sync.newCondition();
    }
    public int getHoldCount() {
        return sync.getHoldCount();
    }
    public boolean isHeldByCurrentThread() {
        return sync.isHeldExclusively();
    }
    public boolean isLocked() {
        return sync.isLocked();
    }
    public final boolean isFair() {
        return sync instanceof FairSync;
    }
    protected Thread getOwner() {
        return sync.getOwner();
    }
    public final boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }
    public final boolean hasQueuedThread(Thread thread) {
        return sync.isQueued(thread);
    }
    public final int getQueueLength() {
        return sync.getQueueLength();
    }
    protected Collection<Thread> getQueuedThreads() {
        return sync.getQueuedThreads();
    }
    public boolean hasWaiters(Condition condition) {
        if (condition == null)
            throw new NullPointerException();
        if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
            throw new IllegalArgumentException("not owner");
        return sync.hasWaiters((AbstractQueuedSynchronizer.ConditionObject)condition);
    }
    public int getWaitQueueLength(Condition condition) {
        if (condition == null)
            throw new NullPointerException();
        if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
            throw new IllegalArgumentException("not owner");
        return sync.getWaitQueueLength((AbstractQueuedSynchronizer.ConditionObject)condition);
    }
    protected Collection<Thread> getWaitingThreads(Condition condition) {
        if (condition == null)
            throw new NullPointerException();
        if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
            throw new IllegalArgumentException("not owner");
        return sync.getWaitingThreads((AbstractQueuedSynchronizer.ConditionObject)condition);
    }
    public String toString() {
        Thread o = sync.getOwner();
        return super.toString() + ((o == null) ?
                                   "[Unlocked]" :
                                   "[Locked by thread " + o.getName() + "]");
    }
}

可以看到reentranLock非常简单,其实现完全依靠AbstractQueuedSynchronizer类。

类reentrantlock锁

public class MyLock implements Lock,Serializable{
    private static final long serialVersionUID = 5693186507433507039L;
    private final Sync sync;
    static class Sync extends AbstractQueuedSynchronizer{
        private static final long serialVersionUID = -7710824394661306145L;
        //加锁
        @Override
        protected boolean tryAcquire(int arg) {
            final Thread current = Thread.currentThread();
            int status = getState();//
            if(0==status) {
                if(compareAndSetState(0, 1)) {
                    return true;
                }
            }else if(current == getExclusiveOwnerThread()) {
                //可重入
                int nextc = status + arg;
                setState(nextc);
                return true;
            }
            return false;
        }
        //逐步释放锁
        @Override
        protected boolean tryRelease(int arg) {
            final Thread current = Thread.currentThread();
            int status = getState();
            int nextc = status - arg;
            if(current != getExclusiveOwnerThread()) {
                //throw new RuntimeException();
            }
            if(nextc == 0) {
                setExclusiveOwnerThread(null);
                setState(0);
                return true;
            }
            setState(nextc);
            return false;
        }
        final ConditionObject newCondition() {
            return new ConditionObject();
        }
    }
    MyLock(){
        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.tryAcquireNanos(1, unit.toNanos(time));
    }
    @Override
    public void unlock() {
        sync.release(1);
    }
    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }
}

上面的MyLock是自定义实现的一个独占式锁,通过tryAcquire操作判断线程是否获取到了同步状态,这个方法是MyLock自身实现的一个方法。通过tryRelease方法判断是否释放了同步状态。通过子类自定义实现获取和释放的操作最终调用AQS中的方法实现锁操作。

AQS

AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等);
AQS解决了子类实现同步器时涉及当的大量细节问题,例如获取同步状态、FIFO同步队列。基于AQS来构建同步器可以带来很多好处。它不仅能够极大地减少实现工作,而且也不必处理在多个位置上发生的竞争问题。在基于AQS构建的同步器中,只能在一个时刻发生阻塞,从而降低上下文切换的开销,提高了吞吐量。同时在设计AQS时充分考虑了可伸缩行,因此J.U.C中所有基于AQS构建的同步器均可以获得这个优势。 AQS的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。

AQS源码分析

AQS类结构

156810789406228

从图中可以看出来,AbstractQueuedSynchronizer内部维护了一个Node节点类和一个ConditionObject内部类。Node相关。内部类是一个双向的FIFO队列,用来保存阻塞中的线程以及获取同步状态的线程,而ConditionObject对应的是Lock中的等待和通知机制。

node类结构

156810823484862

同步队列

除了Node节点的这个FIFO队列,还有一个重要的概念就是waitStatus一个volatile关键字修饰的节点等待状态。在AQS中waitstatus有五种值:

  • SIGNAL 值为-1、后继节点的线程处于等待的状态、当前节点的线程如果释放了同步状态或者被取消、会通知后继节点、后继节点会获取锁并执行(当一个节点的状态为SIGNAL时就意味着在等待获取同步状态,前节点是头节点也就是获取同步状态的节点)

  • CANCELLED 值为1、因为超时或者中断,结点会被设置为取消状态,被取消状态的结点不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态。处于这种状态的结点会被踢出队列,被GC回收(一旦节点状态值为1说明被取消,那么这个节点会从同步队列中删除)

  • CONDITION 值为-2、节点在等待队列中、节点线程等待在Condition、当其它线程对Condition调用了singal()方法该节点会从等待队列中移到同步队列中

  • PROPAGATE 值为-3、表示下一次共享式同步状态获取将会被无条件的被传播下去(读写锁中存在的状态,代表后续还有资源,可以多个线程同时拥有同步状态)

  • initial 值为0、表示当前没有线程获取锁(初始状态)

了解了节点等待的状态以及同步队列的作用,AQS中还通过了一个volatile关键字修饰的status对象用来管理锁的状态并提供了getState()、setState()、compareAndSetStatus()三个方法改变status的状态。知道了这些就可以开始真正看AQS是如何处理没有获取锁的线程的。

源码分析

接下来就从源码的角度了解AQS中的锁操作机制:

aquire独占式获取锁流程

acquire(int arg):独占式的获取锁,此方法不响应中断,在这过程中中断,线程不会从同步队列中移除也不会立马中断

		public final void acquire(int arg){
              if(!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE),arg)){
                         selfInterrupt();//如果这个过程中出现中断,在整个过程结束后再自我中断
              }
         }

acquire方法代码很少,但是它做了很多事,首先前面介绍过tryAcquire()方法是子类实现的具体获取锁的方法,当锁获取到了就会立刻退出if条件也就代表获取锁具体的就是啥也不干。那么看锁获取失败具体干了啥呢。首先是addWaiter(Node.EXCLUSIVE)方法

addWaiter(Node mode):往同步队列中添加元素

		private Node addWaiter(Node mode){
        //通过当前线程和锁模式创建了一个Node节点
            Node node = new Node(Thread.currentThread(),mode);
        //获取尾节点
            Node pred = tail;
            if(pred != null){
               node.prev = pred;//新增的节点每次都是加在同步队列的尾部
               //通过CAS操作设置尾节点防止线程不安全
               if(compareAndSetTail(pred,node)){
                    pred.next = node;
                    return node;
               }
            }
            enq(node);//防止CAS操作失败,再次处理
            return node;
         }

addWaiter方法主要做的就是创建一个节点,如果通过CAS操作成功就直接将节点加入同步队列的尾部,否则需要enq方法的帮忙再次进行处理。设置尾节点的操作必须是CAS类型的,因为会有多个线程同时去获取同步状态防止并发不安全。

添加到队列尾节点操作

enq(Node node):在addWaiter方法处理失败的时候进一步进行处理

		private Node enq(final Node node){
         //死循环【发现很多的底层死循环都是这么写不知道是不是有什么优化点】
           for(;;){
              Node t = tail;
              if(t == null){//如果尾节点为null
                if(compareAndSetHead(new Node())){//创建一个新的节点并添加到队列中初始化
                    tail = head;
                }else{
                    node.prev = t;
             //还是通过CAS操作添加到尾部
                    if(compareAndSetTail(t,node)){
                         t.next = node;
                         return t;
                    }
                }
              }
           }
        }

enq方法就是通过死循环,不断的通过CAS操作设置尾节点,直到添加成功才返回。

acquireQueued(final Node node,int arg):当线程获取锁失败并加入同步队列以后,就进入了一个自旋的状态,如果获取到了这个状态就退出阻塞状态否则就一直阻塞

		final boolean acquireQueued(final Node node,int arg){
           boolean failed = true;//用来判断是否获取了同步状态
              try{
                   boolean interrupted = false;//判断自旋过程中是否被中断过
                   for(;;){
                       final Node p = node.predecessor();//获取前继节点
                       if(p == head && tryAcquire(arg)){//如果当前的这个节点的前继节点是头节点就去尝试获取了同步状态
                           setHead(node);//设为头节点
                           p.next = null;
                           failed = false;//代表获取了同步状态
                           return interrupted;
                       }
              //判断自己是否已经阻塞了检查这个过程中是否被中断过
                       if(shouldParkAfterFailedAcquire(p,node) && parkAndCheckInterrupt() ){
                           interrupted = true;
                       }
                   }finally{
                       if(failed){
                          cancelAcquired(node);
                       }
                   }
               }
        }

acquireQueued方法主要是让线程通过自旋的方式去获取同步状态,当然也不是每个节点都有获取的资格,因为是FIFO先进先出队列,acquireQueued方法保证了只有头节点的后继节点才有资格去获取同步状态,如果线程可以休息了就让该线程休息然后记录下这个过程中是否被中断过,当线程获取了同步状态就会从这个同步队列中移除这个节点。同时还会设置获取同步状态的线程为头节点,在设置头节点的过程中不需要任何的同步操作,因为独占式锁中能获取同步状态的必定是同一个线程。

156818629589910

设置首节点

156818649411724

同步队列中节点自旋

shouldParkAfterFailedAcquire(Node node,Node node):判断一个线程是否阻塞

		private static boolean shouldPArkAfterFailedAcquire(Node pred,Node node){
            int ws = pred.waitStatus;//获取节点的等待状态
            if(ws == Node.SIGNAL){//如果是SIGNAL就代表当头节点释放后,这个节点就会去尝试获取状态
                   return true;//代表阻塞中
            }
            if(ws > 0){//代表前继节点放弃了
              do {
                  node.prev = pred = pred.prev;//循环不停的往前找知道找到节点的状态是正常的
              }while(pred.waitStatus > 0 );
               pred.next = node;
              }else{
                 compareAndSetWaitStatus(pred,ws,Node.SIGNAL);//通过CAS操作设置状态为SIGNAL
              }
              return false;
        }

整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,也就是只有当前驱节点为SIGNAL时这个线程才可以进入等待状态。

parkAndCheckInterrupt():前面的方法是判断是否阻塞,而这个方法就是真正的执行阻塞的方法同时返回中断状态

	private final boolean parkAndCheckInterupt(){
             LockSupport.park(this);//阻塞当前线程
             return Thread.interrupted();//返回中断状态
	}

经过了上面的这么多方法,再次回头看acquire方法的时候。会发现其实整个流程也没有想象中的那么难以理解。acquire方法流程

  • 首先通过子类判断是否获取了锁,如果获取了就什么也不干。
  • 如果没有获取锁、通过线程创建节点加入同步队列的队尾。
  • 当线程在同步队列中不断的通过自旋去获取同步状态,如果获取了锁,就把其设为同步队列中的头节点,否则在同步队列中不停的自旋等待获取同步状态。
  • 如果在获取同步状态的过程中被中断过最后自行调用interrupted方法进行中断操作。

acquire也就是独占式获取锁的整个流程

156818710483100

aquire独占式释放锁流程

release(int arg):独占式的释放锁

	  public final boolean release(int arg){
          if(tryRelease(arg)){//子类自定义实现
              Node h = head;
              if(h != null && h.waitStatus != 0){
                   unparkSuccessor(h);//唤醒下一个节点
              }
              return true;
          }
          return false;
        }	

释放锁的流程很简单,首先子类自定义的方法如果释放了同步状态,如果头节点不为空并且头节点的等待状态不为0就唤醒其后继节点。主要依赖的就是子类自定义实现的释放操作。

unparkSuccessor(Node node):唤醒后继节点获取同步状态

	   private void unparkSuccessor(Node node){
           //获取头节点的状态
           int ws = node.waitStatus;
           if(ws < 0){
              compareAndSetWaitStatus(node,ws,0);//通过CAS将头节点的状态设置为初始状态
           }
           Node s = node.next;//后继节点
           if(s == null || s.waitStatus >0){//不存在或者已经取消
              s = null;
              for(Node t = tail;t != null && t != node;t = t.prev){//从尾节点开始往前遍历,寻找离头节点最近的等待状态正常的节点
                 if(t.waitStatus <= 0){
                    s = t;
                 }
              }
           }
           if(s != null){
              LockSupport.unpark(s.thread);//真正的唤醒操作
           }
        }

唤醒操作,通过判断后继节点是否存在,如果不存在就寻找等待时间最长的适合的节点将其唤醒唤醒操作通过LockSupport中的unpark方法唤醒底层也就是unsafe类的操作。

以上就是独占式的获取锁以及释放锁的过程总结的来说:线程获取锁,如果获取了锁就啥也不干,如果没获取就创造一个节点通过compareAndSetTail(CAS操作)操作的方式将创建的节点加入同步队列的尾部,在同步队列中的节点通过自旋的操作不断去获取同步状态【当然由于FIFO先进先出的特性】等待时间越长就越先被唤醒。当头节点释放同步状态的时候,首先查看是否存在后继节点,如果存在就唤醒自己的后继节点,如果不存在就获取等待时间最长的符合条件的线程。

aquire共享式获取锁流程

acquireShared(int arg):共享式的获取锁

		public final void acquireShared(int arg){
           //子类自定义实现的获取状态【也就是当返回为>=0的时候就代表获取锁】
                 if(tryAcquireShared(arg) < 0){
                         doAcquiredShared(arg);//具体的处理没有获取锁的线程的方法
                 }
         }

doAcquiredShared(int arg):处理未获取同步状态的线程

	     private void doAcquire(int arg){
            final Node node = addWaiter(Node.SHARED);//创建一个节点加入同步队列尾部
            boolean failed = true;//判断获取状态
            try{
                boolean interrupted = false;//是否被中断过
                for(;;){
                    final Node p =node.predecessor();//获取前驱节点
                    if(p == head){
                       int r = tryAcquireShared(arg);//获取同步状态
                       if(r >= 0 ){//大于0代表获取到了
                          setHeadAndPropagate(node,r);//设置为头节点并且如果有多余资源一并唤醒
                          p.next = null;
                          if(interrupted){
                            selfInterrupted();//自我中断
                         }
                         failed = false;
                         return;
                      }
                  }
           //判断线程是否可以进行休息如果可以休息就调用park方法
                 if(shouldParkAfterFailedAcquire(p,node) && parkAndCheckInterrupt()){
                    interrupted = true;
                 }
            }
             }finally{
                 if(failed){
                     cancelAcquire(node);
                 }
          }
      }

共享式获取锁和独占式唯一的区别在于setHeadAndPropagate这个方法,独占式的锁会去判断是否为后继节点,只有后继节点才有资格在头节点释放了同步状态以后获取到同步状态而共享式的实现依靠着setHeadAndPropagate这个方法

setHeadAndPorpagate(Node node,int arg):获取共享同步状态以后的操作

		private void setHeadAndPropaGate(Node node,int propagate){
                 Node h = head;
                 setHead(node);//设置为头节点
                if(propagate >0 || h == null || h.waitStatus < 0){//大于0代表还有其他资源一并可以唤醒
                         Node s = node.next;//下一个节点
                          if(s == null || s.isShared()){
                                  doReleaseShared();
                         }
                 }
         }	

这个方法主要的目的就是将获取到同步状态的节点设置为头节点、如果存在多个资源就将多个资源一并唤醒

doReleaseShared():唤醒后继节点

	   private void doReleaseShared(int arg){
           for(;;){
              Node h = head;
              if(h != null && h != tail){
                  int ws = h.waitStatus;//获取头节点的等待状态
                  if(!compareAndSetWaitStatus(h,Node.SIGNAL,0)){//设置不成功就一直进行设置
                       continue;
                  }
                  unparkSuccessor(h);//唤醒后继节点
              }else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                   continue;                
             }
              if (h == head)                   
                 break;
        }

OK,至此,共享式的获取锁也研究过了。让我们再梳理一下它的流程

  • tryAcquireShared()尝试获取资源,成功则直接返回;
  • 失败则通过doAcquireShared()进入同步队列中,直到头节点释放同步状态后唤醒后继节点并成功获取到资源才返回。整个等待过程也是忽略中断的。
    其实跟acquire()的流程大同小异,只不过多了个自己拿到资源后,还会去唤醒后继队友的操作(这才是共享嘛)
aquire共享式释放锁流程

releaseShared():释放共享同步状态

		public final boolean releaseShared(int arg){
        //子类自定义释放锁操作true代表释放
            if(tryReleaseShared(arg)){
                doReleaseShared();//处理释放的操作
                return true;
            }
         }

通过子类自定义实现的释放锁操作判断,如果未释放就什么也不干,而doReleased方法就是去唤醒当前的后继节点。

容器类

BlockingQueue

阻塞队列最核心的功能是,能够可阻塞式的插入和删除队列元素。当前队列为空时,会阻塞消费数据的线程,直至队列非空时,通知被阻塞的线程;当队列满时,会阻塞插入数据的线程,直至队列未满时,通知插入数据的线程(生产者线程)

157555197336406

从上图我们可以很清楚看到,通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出;

157555198437348

如上图所示:当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。

157555199445430

如上图所示:当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。

ArrayBlockingQueue

属性

ArrayBlockingQueue的主要属性如下:

        // 底层存储元素的数组。final修饰说明一旦初始化,容量不可变,有界的
        final Object[] items;
        //下一个take, poll, peek or remove操作的index位置
        int takeIndex;
        //下一个put, offer, or add操作的index位置
        int putIndex;
        /** Number of elements in the queue */
        int count;
        /**
         *ArrayBlockingQueue并发控制采用的是两个Condition,这也是其性能瓶颈所在
         *put和take都需要获取同一个lock
         */
        //并发控制的锁
        final ReentrantLock lock;
        //获取操作等待条件
        private final Condition notEmpty;
        //插入操作等待条件
        private final Condition notFull;

关于Condition:

每一个 Lock 可以有任意数据的 Condition 对象, Condition 是与 Lock 绑定的

我们知道一个 Condition 可以在多个地方被 await() ,那么就需要一个FIFO的结构将这些 Condition 串联起来,然后根据需要唤醒一个或者多个(通常是所有)。所以在 Condition 内部就需要一个FIFO的队列

从源码中可以看出ArrayBlockingQueue内部是采用数组进行数据存储的(属性items),为了保证线程安全,采用的是ReentrantLock lock,为了保证可阻塞式的插入删除数据利用的是Condition,当获取数据的消费者线程被阻塞时会将该线程放置到notEmpty等待队列中,当插入数据的生产者线程被阻塞时,会将该线程放置到notFull等待队列中。而notEmpty和notFull等中要属性在构造方法中进行创建:

构造方法

ArrayBlockingQueue有提供三个构造方法;

    //带初始大小的阻塞队列,默认非公平
    public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
    }

    //带初始大小且是否公平锁的堵塞队列
    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull = lock.newCondition();
    }

    //带初始大小且是否公平锁,包含初始元素的队列
    public ArrayBlockingQueue(int capacity, boolean fair,
                              Collection<? extends E> c) {
        this(capacity, fair);
        final ReentrantLock lock = this.lock;
        lock.lock(); // Lock only for visibility, not mutual exclusion
        try {
            int i = 0;
            try {
                for (E e : c) {
                    checkNotNull(e);
                    items[i++] = e;
                }
            } catch (ArrayIndexOutOfBoundsException ex) {
                throw new IllegalArgumentException();
            }
            count = i;
            putIndex = (i == capacity) ? 0 : i;
        } finally {
            lock.unlock();
        }
    }

主要方法
抛出异常 特殊值 阻塞 超时
插入 add(e) offer(e) put(e) offer(e,time,unit)
移除 remove() poll() take() poll(time,unit)
检查 element() peek()/isEmpty()
  • add方法,不太常用,
    调用父类的add(E e)方法,父类(AbstractQueue)方法调用的是Queue接口定义的offer(E e)方法,offer(E e)方法由ArrayBlockingQueue实现。
    public boolean add(E e) {
        return super.add(e);
    }

    public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }
  • offer方法,
    offer方法有两个
    不带超时:队列满时,直接返回false。
    带超时:队列满时,阻塞直到队列可用。
    里面最核心的都用到入队操作enqueue(e),来看下这三个方法
    //不带超时offer方法
    public boolean offer(E e) {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count == items.length)//判断队列是不是已满,已满直接返回false
                return false;
            else {
                //未满入队操作,返回true
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }

    //带超时offer方法
    public boolean offer(E e, long timeout, TimeUnit unit)
            throws InterruptedException {
        checkNotNull(e);
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        //可中断加锁
        lock.lockInterruptibly();
        try {
            //队列已满死循环,直到超时为止
            while (count == items.length) {
                if (nanos <= 0)
                    return false;
                //用condition类,计算等待时间
                nanos = notFull.awaitNanos(nanos);
            }
            //队列未满入队操作
            enqueue(e);
            return true;
        } finally {
            lock.unlock();
        }
    }
  • enqueue方法
    //入队操作
    private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        //如果入队之后队满,将数组下标置为从0开始,防止边界溢出
        if (++putIndex == items.length)
            putIndex = 0;
        //队列元素自增
        count++;
        //通知takes阻塞队列,可以取
        notEmpty.signal();
    }

enqueue方法的逻辑同样也很简单,先完成插入数据,(数组中添加数据),然后通知被阻塞的消费者线程,当前队列中有数据可供消费(notEmpty.signal())

  • put方法

该方法的逻辑很简单,当队列已满时(count == items.length)将线程移入到notFull等待队列中,如果当前满足插入数据的条件,就可以直接调用enqueue(e)插入数据元素

    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            //队列已满则一直堵塞
            while (count == items.length)
                notFull.await();
            //入队操作
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }
  • poll方法
    poll方法也有两个,带超时不带超时
    //不带超时
    public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return (count == 0) ? null : dequeue();
        } finally {
            lock.unlock();
        }
    }

    //带超时
    public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0) {
                if (nanos <= 0)
                    return null;
                nanos = notEmpty.awaitNanos(nanos);
            }
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
  • take方法
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

take方法也主要做了两步:1. 如果当前队列为空的话,则将获取数据的消费者线程移入到等待队列中;2. 若队列不为空则获取数据,即完成出队操作dequeue。

  • dequeue方法
    private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        //获取数据
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        //通知被阻塞的生产者线程
        notFull.signal();
        return x;
    }

dequeue方法也主要做了两件事情:1. 获取队列中的数据,即获取数组中的数据元素((E) items[takeIndex]);2. 通知notFull等待队列中的线程,使其由等待队列移入到同步队列中,使其能够有机会获得lock,并执行完成功退出。

  • peek方法
    public E peek() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return itemAt(takeIndex);
        } finally {
            lock.unlock();
        }
    }

从以上分析,可以看出put和take方法主要是通过condition的通知机制来完成可阻塞式的插入数据和获取数据。

LinkedBlockingQueue

LinkedBlockingQueue是用链表实现的有界阻塞队列,当构造对象时为指定队列大小时,队列默认大小为Integer.MAX_VALUE。从它的构造方法可以看出:

    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }
属性
    /**
     * Current number of elements
     */
    private final AtomicInteger count = new AtomicInteger();
    /**
     * Head of linked list.
     * Invariant: head.item == null
     */
    transient Node<E> head;
    /**
     * Tail of linked list.
     * Invariant: last.next == null
     */
    private transient Node<E> last;
    /**
     * Lock held by take, poll, etc
     */
    private final ReentrantLock takeLock = new ReentrantLock();
    /**
     * Wait queue for waiting takes
     */
    private final Condition notEmpty = takeLock.newCondition();
    /**
     * Lock held by put, offer, etc
     */
    private final ReentrantLock putLock = new ReentrantLock();
    /**
     * Wait queue for waiting puts
     */
    private final Condition notFull = putLock.newCondition();

可以看出与ArrayBlockingQueue主要的区别是,LinkedBlockingQueue在插入数据和删除数据时分别是由两个不同的lock(takeLock和putLock)来控制线程安全的,因此,也由这两个lock生成了两个对应的condition(notEmpty和notFull)来实现可阻塞的插入和删除数据。并且,采用了链表的数据结构来实现队列,Node结点的定义为:

    static class Node<E> {
        E item;
        /**
         * One of:
         * - the real successor Node
         * - this Node, meaning the successor is head.next
         * - null, meaning there is no successor (this is the last node)
         */
        Node<E> next;

        Node(E x) {
            item = x;
        }
    }
主要方法
  • put方法
    public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            //如果队列已满,则阻塞当前线程,将其移入等待队列
            while (count.get() == capacity) {
                notFull.await();
            }
            //入队操作,插入数据
            enqueue(node);
            c = count.getAndIncrement();
            //若队列满足插入数据的条件,则通知被阻塞的生产者线程
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }

put方法的逻辑也同样很容易理解,可见注释。基本上和ArrayBlockingQueue的put方法一样。

  • take方法
    public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            //当前队列为空,则阻塞当前线程,将其移入到等待队列中,直至满足条件
            while (count.get() == 0) {
                notEmpty.await();
            }
            //移除队头元素,获取数据
            x = dequeue();
            c = count.getAndDecrement();
            //如果当前满足移除元素的条件,则通知被阻塞的消费者线程
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

ConcurrentHashMap

1.8版本的ConcurrentHashMap舍弃了segment,并且大量使用了synchronized,以及CAS无锁操作以保证ConcurrentHashMap操作的线程安全性。至于为什么不用ReentrantLock而是Synchronzied呢?实际上,synchronzied做了很多的优化,包括偏向锁,轻量级锁,重量级锁,可以依次向上升级锁状态,但不能降级(具体由兴趣同学可以深入了解下Synchronzied的原理),因此,使用synchronized相较于ReentrantLock的性能会持平甚至在某些情况更优,具体的性能测试可以去网上查阅一些资料。另外,底层数据结构改变为采用数组+链表+红黑树的数据形式。

157555206205625

关键属性

  • table
    volatile Node[] table

    装载Node的数组,作为ConcurrentHashMap的数据容器,采用懒加载的方式,直到第一次插入数据的时候才会进行初始化操作,数组的大小总是为2的幂次方。

  • nextTable
    volatile Node[] nextTable;

    扩容时使用,平时为null,只有在扩容的时候才为非null

  • sizeCtl
    volatile int sizeCtl
    该属性用来控制table数组的大小,根据是否初始化和是否正在扩容有几种情况:
    当值为负数时:如果为-1表示正在初始化,如果为-N则表示当前正有N-1个线程进行扩容操作;
    当值为正数时:如果当前数组为null的话表示table在初始化过程中,sizeCtl表示为需要新建数组的长度;
    若已经初始化了,表示当前数据容器(table数组)可用容量也可以理解成临界值(插入节点数超过了该临界值就需要扩容),具体指为数组的长度n 乘以 加载因子loadFactor;
    当值为0时,即数组长度为默认初始值。

  • sun.misc.Unsafe U
    在ConcurrentHashMapde的实现中可以看到大量的U.compareAndSwapXXXX的方法去修改ConcurrentHashMap的一些属性,这个其实在我们上节课由提道。这些方法实际上是利用了CAS算法保证了线程安全性,这是一种乐观策略,假设每一次操作都不会产生冲突,当且仅当冲突发生的时候再去尝试。而CAS操作依赖于现代处理器指令集,通过底层CMPXCHG指令实现。CAS(V,O,N)核心思想为:若当前变量实际值V与期望的旧值O相同,则表明该变量没被其他线程进行修改,因此可以安全的将新值N赋值给变量;若当前变量实际值V与期望的旧值O不相同,则表明该变量已经被其他线程做了处理,此时将新值N赋给变量操作就是不安全的,在进行重试。而在大量的同步组件和并发容器的实现中使用CAS是通过sun.misc.Unsafe类实现的,该类提供了一些可以直接操控内存和线程的底层操作,可以理解为java中的“指针”。该成员变量的获取是在静态代码块中:

static {
     try {
         U = sun.misc.Unsafe.getUnsafe();
         .......
     } catch (Exception e) {
         throw new Error(e);
     }
 }

关键内部类

  • Node
    Node类实现了Map.Entry接口,主要存放key-value对,并且具有next域
    static class Node<K, V> implements Map.Entry<K, V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K, V> next;

        Node(int hash, K key, V val, Node<K, V> next) {
            this.hash = hash;
            this.key = key;
            this.val = val;
            this.next = next;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return val;
        }

        public final int hashCode() {
            return key.hashCode() ^ val.hashCode();
        }

        public final String toString() {
            return key + "=" + val;
        }

        public final V setValue(V value) {
            throw new UnsupportedOperationException();
        }

        public final boolean equals(Object o) {
            Object k, v, u;
            Map.Entry<?, ?> e;
            return ((o instanceof Map.Entry) &&
                    (k = (e = (Map.Entry<?, ?>) o).getKey()) != null &&
                    (v = e.getValue()) != null &&
                    (k == key || k.equals(key)) &&
                    (v == (u = val) || v.equals(u)));
        }

        /**
         * Virtualized support for map.get(); overridden in subclasses.
         */
        Node<K, V> find(int h, Object k) {
            Node<K, V> e = this;
            if (k != null) {
                do {
                    K ek;
                    if (e.hash == h &&
                            ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                } while ((e = e.next) != null);
            }
            return null;
        }
    }

另外可以看出很多属性都是用volatile进行修饰的,也就是为了保证内存可见性。

  • TreeNode
    树节点,继承于承载数据的Node类。而红黑树的操作是针对TreeBin类的,从该类的注释也可以看出,也就是TreeBin会将TreeNode进行再一次封装
    static final class TreeNode<K, V> extends Node<K, V> {
        TreeNode<K, V> parent;  // red-black tree links
        TreeNode<K, V> left;
        TreeNode<K, V> right;
        TreeNode<K, V> prev;    // needed to unlink next upon deletion
        boolean red;
         ......
    }
  • TreeBin
    这个类并不负责包装用户的key、value信息,而是包装的很多TreeNode节点。实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象,而不是TreeNode对象。
    static final class TreeBin<K, V> extends Node<K, V> {
        TreeNode<K, V> root;
        volatile TreeNode<K, V> first;
        volatile Thread waiter;
        volatile int lockState;
        // values for lockState
        static final int WRITER = 1; // set while holding write lock
        static final int WAITER = 2; // set when waiting for write lock
        static final int READER = 4; // increment value for setting read lock
         ......
    }
  • ForwardingNode
    在扩容时才会出现的特殊节点,其key,value,hash全部为null。并拥有nextTable指针引用新的table数组。
    static final class ForwardingNode<K, V> extends Node<K, V> {
        final Node<K, V>[] nextTable;

        ForwardingNode(Node<K, V>[] tab) {
            super(MOVED, null, null, null);
            this.nextTable = tab;
        }
         ......
    }

主要方法

提供了5个构造方法

// 1. 构造一个空的map,即table数组还未初始化,初始化放在第一次插入数据时,默认大小为16
ConcurrentHashMap()
// 2. 给定map的大小
ConcurrentHashMap(int initialCapacity) 
// 3. 给定一个map
ConcurrentHashMap(Map<? extends K, ? extends V> m)
// 4. 给定map的大小以及加载因子
ConcurrentHashMap(int initialCapacity, float loadFactor)
// 5. 给定map大小,加载因子以及并发度(预计同时操作数据的线程)
ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel)
ConcurrentHashMap(int initialCapacity)
public ConcurrentHashMap(int initialCapacity) {
    //1. 小于0直接抛异常
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    //2. 判断是否超过了允许的最大值,超过了话则取最大值,否则再对该值进一步处理
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    //3. 赋值给sizeCtl
    this.sizeCtl = cap;
}

这段代码的逻辑请看注释,很容易理解,如果小于0就直接抛出异常,如果指定值大于了所允许的最大值的话就取最大值,否则,在对指定值做进一步处理。最后将cap赋值给sizeCtl,关于sizeCtl的说明请看上面的说明,当调用构造器方法之后,sizeCtl的大小应该就代表了ConcurrentHashMap的大小,即table数组长度。

tableSizeFor()
private static final int tableSizeFor(int c) {
    int n = c - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

通过注释就很清楚了,该方法会将调用构造器方法时指定的大小转换成一个2的幂次方数,也就是说ConcurrentHashMap的大小一定是2的幂次方,比如,当指定大小为18时,为了满足2的幂次方特性,实际上concurrentHashMapd的大小为2的5次方(32)。另外,需要注意的是,调用构造器方法的时候并未构造出table数组(可以理解为ConcurrentHashMap的数据容器),只是算出table数组的长度,当第一次向ConcurrentHashMap插入数据的时候才真正的完成初始化创建table数组的工作。

initTable()
    private final Node<K, V>[] initTable() {
        Node<K, V>[] tab;
        int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                // 1. 保证只有一个线程正在进行初始化操作
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        // 2. 得出数组的大小
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        // 3. 这里才真正的初始化数组
                        Node<K, V>[] nt = (Node<K, V>[]) new Node<?, ?>[n];
                        table = tab = nt;
                        // 4. 计算数组中可用的大小:实际大小n*0.75(加载因子)
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

代码的逻辑请见注释,有可能存在一个情况是多个线程同时走到这个方法中,为了保证能够正确初始化,在第1步中会先通过if进行判断,若当前已经有一个线程正在初始化即sizeCtl值变为-1,这个时候其他线程在If判断为true从而调用Thread.yield()让出CPU时间片。正在进行初始化的线程会调用U.compareAndSwapInt方法将sizeCtl改为-1即正在初始化的状态。另外还需要注意的事情是,在第四步中会进一步计算数组中可用的大小即为数组实际大小n乘以加载因子0.75.可以看看这里乘以0.75是怎么算的,0.75为四分之三,这里n - (n >>> 2)是不是刚好是n-(1/4)n=(3/4)n,挺有意思的吧:)。如果选择是无参的构造器的话,这里在new Node数组的时候会使用默认大小为DEFAULT_CAPACITY(16),然后乘以加载因子0.75为12,也就是说数组的可用大小为12。

put()

使用ConcurrentHashMap最常用,主要的也应该是put和get方法了吧,我们先来看看put方法是怎样实现的。调用put方法时实际具体实现是putVal方法,源码如下:

    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        //1. 计算key的hash值
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K, V>[] tab = table; ; ) {
            Node<K, V> f;
            int n, i, fh;
            //2. 如果当前table还没有初始化先调用initTable方法将tab进行初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //3. tab中索引为i的位置的元素为null,则直接使用CAS将值插入即可
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                        new Node<K, V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //4. 当前正在扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        //5. 当前为链表,在链表中插入新的键值对
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K, V> e = f; ; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                        ((ek = e.key) == key ||
                                                (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K, V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K, V>(hash, key,
                                            value, null);
                                    break;
                                }
                            }
                        }
                        // 6.当前为红黑树,将新的键值对插入到红黑树中
                        else if (f instanceof TreeBin) {
                            Node<K, V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K, V>) f).putTreeVal(hash, key,
                                    value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                // 7.插入完键值对后再根据实际大小看是否需要转换成红黑树
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        //8.对当前大小进行检查,如果超过了临界值(容量*加载因子)就需要扩容
        addCount(1L, binCount);
        return null;
    }

put方法的代码量有点长,我们按照上面的分解的步骤一步步来看。从整体而言,为了解决线程安全的问题,ConcurrentHashMap使用了synchronzied和CAS的方式。这里我默认大家都是理解过HashMap的底层结构的,ConcurrentHashMap底层结构跟HashMap几乎类似。
ConcurrentHashMap是一个哈希桶数组,如果不出现哈希冲突的时候,每个元素均匀的分布在哈希桶数组中。当出现哈希冲突的时候,是标准的链地址的解决方式,将hash值相同的节点构成链表的形式,称为“拉链法”,另外,在1.8版本中为了防止拉链过长,当链表的长度大于8的时候会将链表转换成红黑树。table数组中的每个元素实际上是单链表的头结点或者红黑树的根节点。当插入键值对时首先应该定位到要插入的桶,即插入table数组的索引i处。那么,怎样计算得出索引i呢?当然是根据key的hashCode值。

  • spread()重哈希,以减小Hash冲突

高级同步对象

atomic和locks包提供的Java类可以满足基本的互斥和同步访问的需求,但这些Java类的抽象层次较低,使用比较复杂。
更简单的做法是使用java.util.concurrent包中的高级同步对象。
我们以信号量(java.util.concurrent.Semaphore类)和(java.util.concurrent.Exchanger类)为例进行说明

Semaphore(信号量)

见名知意:信号量,可近似看作资源池。

信号量一般用来数量有限的资源,每类资源有一个对象的信号量,信号量的值表示资源的可用数量。
在使用资源时,需要从该信号量上获取许可,成功获取许可,资源的可用数-1;完成对资源的使用,释放许可,资源可用数+1; 当资源数为0时,需要获取资源的线程以阻塞的方式来等待资源,或过段时间之后再来检查资源是否可用。
java.util.concurrent.Semaphore类,在创建Semaphore类的对象时指定资源的可用数,相关方法:

  • acquire(),以阻塞方式获取许可
  • tryAcquire(),以非阻塞方式获取许可
  • release(),释放许可。
  • accquireUninterruptibly(),accquire()方法获取许可以的过程可以被中断,如果不希望被中断,使用此方法
public class PrinterManager{  
   private final Semphore semaphore;  
   private final List<Printer> printers = new ArrayList<>():  
   public PrinterManager(Collection<? extends Printer> printers){  
      this.printers.addAll(printers);  
      //这里重载方法,第二个参数为true,以公平竞争模式,防止线程饥饿  
      this.semaphore = new Semaphore(this.printers.size(),true);  
   }  
   public Printer acquirePrinter() throws InterruptedException{  
      semaphore.acquire();  
      return getAvailablePrinter();  
   }  
   public void releasePrinter(Printer printer){  
      putBackPrinter(pinter);  
      semaphore.release();  
   }  
   private synchronized Printer getAvailablePrinter(){  
      printer result = printers.get(0);  
      printers.remove(0);  
      return result;  
   }  
   private synchronized void putBackPrinter(Printer printer){  
      printers.add(printer);  
   }  
}

Exchanger(对象交换器)

适合于两个线程需要进行数据交换的场景。(一个线程完成后,把结果交给另一个线程继续处理)
java.util.concurrent.Exchanger类,提供了这种对象交换能力,两个线程共享一个Exchanger类的对象,一个线程完成对数据的处理之后,调用Exchanger类的exchange()方法把处理之后的数据作为参数发送给另外一个线程。而exchange方法的返回结果是另外一个线程锁提供的相同类型的对象。如果另外一个线程未完成对数据的处理,那么exchange()会使当前线程进入等待状态,直到另外一个线程也调用了exchange方法来进行数据交换。
例:

public class SendAndReceiver{  
   private final Exchanger<StringBuilder> exchanger = new Exchanger<StringBuilder>();  
   private class Sender implements Runnable{  
      public void run(){  
         try{  
            StringBuilder content = new StringBuilder("Hello");  
            content = exchanger.exchange(content);  
         }catch(InterruptedException e){  
            Thread.currentThread().interrupt();  
         }  
      }  
   }  
   private class Receiver implements Runnable{  
      public void run(){  
         try{  
            StringBuilder content = new StringBuilder("World");  
            content = exchanger.exchange(content);  
         }catch(InterruptedException e){  
            Thread.currentThread().interrupt();  
         }  
      }  
   }  
   public void exchange(){  
      new Thread(new Sender()).start();  
      new Thread(new Receiver()).start();  
   }  
}

CountDownLatch(倒数闸门)

见名知意:倒计时锁存器,阻塞主线程

CountDownLatch:CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。

CountDownLatch类只提供了一个构造器:

public CountDownLatch(int count) { };
然后下面这3个方法是CountDownLatch类中最重要的方法 :
public void await() throws InterruptedException { }; //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { }; //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public void countDown() { }; //将count值减1

下面举个例子:

public class Test {
     public static void main(String[] args) {   
         final CountDownLatch latch = new CountDownLatch(2);
         new Thread(){
             public void run() {
                 try {
                     System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
                    Thread.sleep(3000);
                    System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
             };
         }.start();
         new Thread(){
             public void run() {
                 try {
                     System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
                     Thread.sleep(3000);
                     System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
                     latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
             };
         }.start();
         try {
             System.out.println("等待2个子线程执行完毕...");
            latch.await();
            System.out.println("2个子线程已经执行完毕");
            System.out.println("继续执行主线程");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
     }
}

CyclicBarrier(回环栅栏)

见名知意:循环障碍,加法计时器,阻塞所有线程

字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。
CyclicBarrier类位于java.util.concurrent包下,CyclicBarrier提供2个构造器:

public CyclicBarrier(int parties, Runnable barrierAction) {
}
public CyclicBarrier(int parties) {
}

参数parties指让多少个线程或者任务等待至barrier状态;参数barrierAction为当这些线程都达到barrier状态时会执行的内容。
public int await() throws InterruptedException, BrokenBarrierException { };
public int await(long timeout, TimeUnit unit)throws InterruptedException,BrokenBarrierException,TimeoutException { };
第一个版本比较常用,用来挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务;
第二个版本是让这些线程等待至一定的时间,如果还有线程没有到达barrier状态就直接让到达barrier的线程执行后续任务。
比如多线程实现发令枪响了,一群运动员开始跑 ,发令枪就是CyclicBarrier ,运动员会await()直到枪响

下面举个参考范例:
假若有若干个线程都要进行写数据操作,并且只有所有线程都完成写数据操作之后,这些线程才能继续做后面的事情,此时就可以利用CyclicBarrier了

public class Test {
    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier  = new CyclicBarrier(N);
        for(int i=0;i<N;i++)
            new Writer(barrier).start();
    }
    static class Writer extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }
        @Override
        public void run() {
            System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
            try {
                Thread.sleep(5000);      //以睡眠来模拟写入数据操作
                System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println("所有线程写入完毕,继续处理其他任务...");
        }
    }
}

总结

多线程开发中应该优先使用高层API,如果无法满足,使用java.util.concurrent.atomic和java.util.concurrent.locks包提供的中层API,而synchronized和volatile,以及wait,notify和notifyAll等低层API 应该最后考虑。

posted @ 2022-07-12 18:46  Faetbwac  阅读(18)  评论(0编辑  收藏  举报