java并发编程(二)——并发集合类Collections

并发集合类概述

在这里插入图片描述
由于Collections集合类中支持并发的集合有很很多。这里只是提取其中比较常见且重要的集合进行讲解。本文主要从LinkedBlockingQueue、CopyOnWriteArrayList、ConcurrentHashMap三个集合类进行阐述。

LinkedBlockingQueue

LinkedBlockingQueue是在JDK1.5时,随着J.U.C包引入的一种阻塞队列,它实现了BlockingQueue接口,底层基于单链表实现。LinkedBlockingQueue是一种近似有界阻塞队列,为什么说近似?因为LinkedBlockingQueue既可以在初始构造时就指定队列的容量,也可以不指定,如果不指定,它的容量大小默认为Integer.MAX_VALUE。
它维护了两把锁——takeLock和putLock。
takeLock用于控制出队的并发,putLock用于入队的并发。这也就意味着,同一时刻,只能只有一个线程能执行入队/出队操作,其余入队/出队线程会被阻塞;但是,入队和出队之间可以并发执行,即同一时刻,可以同时有一个线程进行入队,另一个线程进行出队,这样就可以提升吞吐量。

LinkedBlockingQueue原理

下边从几种常用的方法的源码分析来学习一下LinkedBlockingQueue的内部原理。

LinkedBlockingQueue的构造函数

//无参构造,默认阻塞队列的容量大小为  Integer.MAX_VALUE
public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
}
	//指定容量
	public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }
	//从已有的集合构造队列,队列的容量为Integer.MAX_VALUE
	public LinkedBlockingQueue(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);    //指定容量大小
        final ReentrantLock putLock = this.putLock;
        putLock.lock(); // 这里加锁仅仅只是为了保证可见性
        try {
            int n = 0;
            for (E e : c) {
                if (e == null)   //队列不能包含null元素
                    throw new NullPointerException();
                if (n == capacity)   //如果队列已经满了,抛出异常
                    throw new IllegalStateException("Queue full");
                enqueue(new Node<E>(e));    //队尾插入元素
                ++n;
            }
            //private final AtomicInteger count = new AtomicInteger();
            count.set(n);   //这里使用一个  原子变量来表示队列中元素的个数。
        } finally {
            putLock.unlock();
        }
    }

根据以上构造函数可知,LinkedBlockingQueue如果不指定容量的大小,则默认的容量大小为Integer.MAX_VALUE。在构造函数LinkedBlockingQueue(Collection<? extends E> c)中,使用原子变量count来 保证出队/入队并发修改元素时数据的一致性。


LinkedBlockingQueue初始化之后,结构如下所示:

在这里插入图片描述

LinkedBlockingQueue的常用方法

由于接口和ArrayBlockingQueue完全一样,所以LinkedBlockingQueue会阻塞线程的方法也一共有4个:put(E e)、offer(E e)、add(E e)和take()、poll(),remove(Object o)我们从源码分析分析。

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上等待
                notFull.await();
            }
            enqueue(node);         //将新的结点连接到“队尾”
            c = count.getAndIncrement();    //c表示入队前的队列元素的个数
        // 如果现队列长度如果小于容量
        // 就再唤醒一个阻塞在notFull条件上的线程
        // 这里为啥要唤醒一下呢?
        // 因为可能有很多线程阻塞在notFull这个条件上的
        // 而取元素时只有取之前队列是满的才会唤醒notFull
        // 为什么队列满的才唤醒notFull呢?
        // 因为唤醒是需要加putLock的,这是为了减少锁的次数
        // 所以,这里索性在放完元素就检测一下,未满就唤醒其它notFull上的线程
        // 说白了,这也是锁分离带来的代价
            if (c + 1 < capacity)         //入队后队列未满,则唤醒一个“入队线程”
                notFull.signal();        
        } finally {
            putLock.unlock();
        }
        if (c == 0)             //队列从null到非空,则唤醒一个“出队线程”
            signalNotEmpty();
    }

在插入元素时,首先要获取“入队锁”,如果队列满了,则当前线程需要在notFull条件队列等待;否则。将新元素连接到队列尾部。
从上面的代码分析中可以得出6点结论:

  1. LinkedBlockingQueue不允许元素为null,这一点在构造方法中也说过了。
  2. 同一时刻,只能有一个线程执行入队操作,因为putLock在将元素插入到队列尾部时加锁了
  3. 如果队列满了,那么将会调用notFull的await()方法将该线程加入到Condition等待队列中。await()方法就会释放线程占有的锁,这将导致之前由于被锁阻塞的入队线程将会获取到锁,执行到while循环处,不过可能因为由于队列仍旧是满的,也被加入到条件队列中。
  4. 一旦一个出队线程取走了一个元素,并通知了入队等待队列中可以释放线程了,那么第一个加入到Condition队列中的将会被释放,那么该线程将会重新获得put锁,继而执行enqueue()方法,将节点插入到队列的尾部
  5. 然后得到插入一个节点之前的元素个数,如果队列中还有空间可以插入,那么就通知notFull条件的等待队列中的线程。
  6. 通知出队线程队列为空了,因为插入一个元素之前的个数为0,而插入一个之后队列中的元素就从无变成了有,就可以通知因队列为空而阻塞的出队线程了。

signalNotEmpty()方法只会在put/take之类的入队方法中才会被调用,并且是当队列元素从无到有的时候。下面是signalNotEmpty()方法的实现:

 private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        //获取takeLock
        takeLock.lock();
        try {
            //释放notEmpty条件队列中的第一个等待线程
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }
offer方法
    public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        final AtomicInteger count = this.count;
        if (count.get() == capacity)
            return false;   //队列满了之后,会直接返回false
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            if (count.get() < capacity) {
                enqueue(node);
                c = count.getAndIncrement();
                if (c + 1 < capacity)
                    notFull.signal();
            }
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
        return c >= 0;
    }

offer方法与put方法的区别就是,offer方法将元素插入队列中,如果成功,则返回true,如果队列已经满了,则返回false。 offer还可以设置等待时间,offer(E o, long timeout, TimeUnit unit)方法,如果在等待时间内还不能将元素加入到队列中,则返回失败。

add方法
public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }

由add方法的源码可以看出,add将元素插入队列中,如果成功返回true;如果当前没有可用空间,则抛IllegalStateException异常。

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(); //c表示出队前的元素个数
            if (c > 1)                //出队前队列非空,则唤醒一个出队线程
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        //如果队列中的元素从满到非满,通知put线程
        if (c == capacity)     
            signalNotFull();
        return x;
    }

take方法的源码与put方法是一样的思想,首先获取“出队锁”,然后取走阻塞队列中排在首位的对象,若阻塞队列为null,进入等待状态,直到有新的数据加入队列。

poll方法
public E poll() {
        final AtomicInteger count = this.count;
        if (count.get() == 0)
            return null;
        E x = null;
        int c = -1;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            if (count.get() > 0) {
                x = dequeue();
                c = count.getAndDecrement();
                if (c > 1)
                    notEmpty.signal();
            }
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

poll方法与offer方法思想一样,当队列中没有元素时,take方法是阻塞,而poll方法直接返回null.poll方法提供了poll(long timeout, TimeUnit unit)方法,设置限时等待,如果在等待时间内有数据可取,则返回去除的数据,如果超时还没数据可取,则返回null。

LinkedBlockingQueue与ArrayBlockingQueue、LinkedBlockingDeque相比

底层数据存储是否有界能否指定公平策略
ArrayBlockingQueue数组必须指定大小,有界全局锁可以指定公平/非公平策略
LinkedBlockingQueue单向链表默认为Integer.MAX_VALUE,近似于无界;也可以指定大小锁分离 入队、出队使用不同的锁不能指定公平/非公平策略
LinkedBlockingDeque双向链表默认为Integer.MAX_VALUE,近似于无界;也可以指定大小全局锁不能指定公平/非公平策略

CopyOnWriteArrayList

CopyOnWriteArrayList是JDK1.5,随着J.U.C引入的一个新的集合。它是为了“读多写少”的情景而生的。
CopyOnWriteArrayList运用了一种 “写时复制”的思想。通俗的理解就是当我们需要修改(增/删/改)列表中的元素时,不直接进行修改,而是先将列表Copy,然后在新的副本上进行修改,修改完成之后,再将引用从原列表指向新列表。这样做的好处是读/写是不会冲突的,可以并发进行,操作还是在原列表,写操作在新列表。仅仅当有多个线程同时进行写操作时,才会进行同步。

CopyOnWriteArrayList原理

CopyOnWriteArrayList构造函数

	//空参构造
   public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
    final void setArray(Object[] a) {
        array = a;
    }
//根据已有集合创建
public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
            elements = c.toArray();
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elements.getClass() != Object[].class)
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        setArray(elements);
    }
//根据已有的数组创建
public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
   }

get方法

    public E get(int index) {
        return get(getArray(), index);
    }
	private E get(Object[] a, int index) {
        return (E) a[index];
    }
     final Object[] getArray() {
        return array;
    }

根据get方法的源码可知,get方法返回的是内部数组对应索引位置的值。

add方法

    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();  //原来的数组
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1); //将旧的数组复制到新的数组中
            newElements[len] = e;  //将元素插入新的数组末尾
            setArray(newElements); //内部array引用指向新的数组
            return true;
        } finally {
            lock.unlock();
        }
    }

由于写的操作都是在新数组上进行,不会对并发时的读操作产生影响。

迭代器

CopyOnWriteArrayList对元素进行迭代时,仅仅返回一个当前内部数组的快照,也就是说,如果此时有其它线程正在修改元素,并不会在迭代中反映出来,因为修改都是在新数组中进行的。

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}
 
static final class COWIterator<E> implements ListIterator<E> {
    /**
     * Snapshot of the array
     */
    private final Object[] snapshot;
    /**
     * Index of element to be returned by subsequent call to next.
     */
    private int cursor;

    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }

    public boolean hasNext() {
        return cursor < snapshot.length;
    }

    public E next() {
        if (!hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }
    
    // ...
}

可以看到,上述iterator方法返回一个迭代器对象——COWIterator,COWIterator的迭代是在旧数组上进行的,当创建迭代器的那一刻就确定了,所以迭代过程中不会抛出并发修改异常——ConcurrentModificationException。
另外,迭代器对象也不支持修改方法,全部会抛出UnsupportedOperationException异常。

CopyOnWriteArrayList特点

1、因为CopyOnWriteArrayList使用了“写时复制”,在进行写操作的时候,内存中是存在两个数组的,如果数组内存占用的太大,那么困难会造成频繁的GC,所以CopyOnWriteArrayList并不适合大数据量的场景。
2、CopyOnWriteArrayList只能保证数据的最终一致性,不能保证数据的实时一致性——读操作读到的数据只是一份快照。所以如果希望写入的数据可以立刻被读到,那CopyOnWriteArrayList并不适合。

ConcurrentHashMap

ConcurrentHashMap简介

之前我们都知道hashmap是线程不安全的,想要了解hashmap的移步:集合——HashMap的一些理解与总结
本篇文章将要介绍的 ConcurrentHashMap 是 HashMap 的并发版本,它是线程安全的,并且在高并发的情境下,性能优于 HashMap 很多。本文基于JDK1.8对ConcurrentHashMap进行学习。

ConcurrentHashMap历史演变

jdk 1.7 采用分段锁技术,整个 Hash 表被分成多个段,每个段中会对应一个 Segment 段锁,段与段之间可以并发访问,但是多线程想要操作同一个段是需要获取锁的。所有的 put,get,remove 等方法都是根据键的 hash 值对应到相应的段中,然后尝试获取锁进行访问。JDK1.7ConcurrentHashMap结构如下图所示。
在这里插入图片描述
jdk 1.8 取消了基于 Segment 的分段锁思想,改用 CAS + synchronized 控制并发操作,在某些方面提升了性能。并且追随 1.8 版本的 HashMap 底层实现,使用数组+链表+红黑树进行数据存储。

JDK1.8的ConcurrentHashMap结构

ConcurrentHashMap内部维护了一个Node类型的数组,也就是table,数组的每一个位置table[i]代表一个桶,会根据键的hash值映射到不同的桶中,table一共包含四种不同类型的桶:Node、TreeBin、ForwardingNode、ReservationNode。由下图可知,ConcurrentHashMap使用数组+链表+红黑树进行数据存储。TreeBin结点所链接的是一棵红黑树,红黑树的结点用TreeNode表示,所以ConcurrentHashMap实际上一共有5中不同类型的Node结点。
为什么要用TreeBin链接红黑树的头结点而不是直接用TreeNode?

因为红黑树的操作比较复杂,包括构建、左旋、右旋、删除,平衡等操作,用一个代理结点TreeBin来包含这些复杂操作,其实是一种“职责分离”的思想,另外TreeBin中也包含了一些加/解锁的操作。

在这里插入图片描述

ConcurrentHashMap构造函数

   public ConcurrentHashMap() {
    }

	/**
 	 * 指定table初始容量的构造器.
	 * tableSizeFor会返回大于入参(initialCapacity + (initialCapacity >>> 1) + 1)的最小2次幂值
 */
	public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }

有上述ConcurrentHashMap的空参构造可知,当我们new ConcurrentHashMap对象的时候,其实什么操作也没做,带参构造也只是计算了一下table初始容量的大小,并没有创建table数组。这是一种“懒加载”的模式,只有到首次插入键值对的时候,才会真正的初始化table数组。至于为什么将容量设置为2的N次幂,之前在HashMap也有提到过,1、让key均匀分布,减少hash冲突。2、当table数组的大小为2的幂次时,通过key.hash & table.length-1这种方式计算出的索引i,当table扩容后(2倍),新的索引要么在原来的位置i,要么是i+n。

ConcurrentHashMap原理

put方法

	public V put(K key, V value) {
        return putVal(key, value, false);
    }
final V putVal(K key, V value, boolean onlyIfAbsent) {
		//对传入的参数进行合法性判断
        if (key == null || value == null) throw new NullPointerException();
        //计算键所对应的 hash 值
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //如果哈希表还未初始化,那么初始化它
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();     // 初始化table懒加载
                //根据键的 hash 值找到哈希数组相应的索引位置
                //如果为空,那么以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
            }
            else if ((fh = f.hash) == MOVED)	//检测到桶结点是 ForwardingNode 类型,协助扩容
                tab = helpTransfer(tab, f);
            else {	//桶结点是普通的结点,锁住该桶头结点并试图在该链表的尾部添加一个节点
                V oldVal = null;	
                synchronized (f) {		// 锁住table[i]结点
                    if (tabAt(tab, i) == f) {		// 再判断一下table[i]是不是第一个结点, 防止其它线程的写修改
                     //向普通的链表中添加元素,无需赘述
                        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;
                                }
                            }
                        }
                        //向红黑树中添加元素,TreeBin 结点的hash值为TREEBIN(-2)
                        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;
                            }
                        }
                    }
                }
                 //binCount != 0 说明向链表或者红黑树中添加或修改一个节点成功
 				 //binCount  == 0 说明 put 操作将一个新节点添加成为某个桶的首节点
                if (binCount != 0) {
                //链表深度超过 8 转换为红黑树
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);	// 链表 -> 红黑树 转换
                    //oldVal != null 说明此次操作是修改操作
         			//直接返回旧值即可,无需做下面的扩容边界检查
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        //CAS 式更新baseCount,并判断是否需要扩容
        addCount(1L, binCount);
        //程序走到这一步说明此次 put 操作是一个添加操作,否则早就 return 返回了
        return null;
    }

初始化table操作源码如下:

private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        //如果表为空才进行初始化操作
        while ((tab = table) == null || tab.length == 0) {
        //sizeCtl 小于零说明已经有线程正在进行初始化操作
        //当前线程应该放弃 CPU 的使用
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            //否则说明还未有线程对表进行初始化,那么本线程就来做这个工作
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            	//保险起见,再次判断下表是否为空
                try {
                    if ((tab = table) == null || tab.length == 0) {
                    //sc 大于零说明容量已经初始化了,否则使用默认容量
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        //根据容量构建数组
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        //计算阈值,等效于 n*0.75
                        sc = n - (n >>> 2);
                    }
                } finally {
                	//设置阈值
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

put操作流程图如下图所示:
在这里插入图片描述
ConcurrentHashMap的put()方法执行流程如下:
1、key或value是否为空,如果是则抛异常NullPointerException
2、判断table是否为空或length==0,如果是,则initTable方法初始化table
3、根据键值key计算hash得到插入数组的索引i,判断table[i]是否为null,如果是则直接向table[i]中添加节点,执行第7步;如果不是,判断table[i]的hash值是否为MOVED(即判断该结点是否为ForwardingNode 结点),如果发现该结点为 ForwardingNode 结点,表明当前的哈希表正在扩容和 rehash,则帮忙扩容。
4、判断table[i]是否为普通链表结点,如果是,则判断key是否存在,如果key存在,则更新value;key不存在,则直接插入到链表尾部
5、判断链表长度是否大于8,如果是,判断table的大小是否大于64,如果table大小大于64,则将链表转换为红黑树;否则,将table扩容
6、如果table[i]不是链表结点,则判断是否为treeBin结点,如果是,则红黑树插入。
7、addCount计数加1并判断是否需要扩容。

get方法

public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());	 // 重新计算key的hash值
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {		 // table[i]就是待查找的项,直接返回
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)		 // hash值<0, 说明遇到特殊结点(非链表结点), 调用find方法查找
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {	// 按链表方式查找
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

get方法的逻辑很简单,首先根据key的hash值计算映射到table的哪个桶——table[i]。
1、如果table[i]的key和待查找key相同,那直接返回;
2、如果table[i]对应的结点是特殊结点(hash值小于0),则通过find方法查找;
3、如果table[i]对应的结点是普通链表结点,则按链表方式查找。

ConcurrentHashMap的扩容

扩容的时机

前面已经说了,这里不再赘述。见流程图。

扩容方法

/**
 * 尝试对table数组进行扩容.
 *
 * @param 待扩容的大小
 */
private final void tryPresize(int size) {
 		// 视情况将size调整为2的幂次
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
            tableSizeFor(size + (size >>> 1) + 1);
        int sc;
        while ((sc = sizeCtl) >= 0) {
            Node<K,V>[] tab = table; int n;
            // table还未初始化,则先进行初始化
            if (tab == null || (n = tab.length) == 0) {
                n = (sc > c) ? sc : c;
                if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                    try {
                        if (table == tab) {
                            @SuppressWarnings("unchecked")
                            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                            table = nt;
                            sc = n - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                }
            }
            //c <= sc说明已经被扩容过了;n >= MAXIMUM_CAPACITY说明table数组已达到最大容量
            else if (c <= sc || n >= MAXIMUM_CAPACITY)
                break;
            //进行table扩容
            else if (tab == table) {
                int rs = resizeStamp(n); //根据容量n生成一个随机数,唯一标识本次扩容操作
                 if (sc < 0) {  // sc < 0 表明此时有别的线程正在进行扩容
                    Node<K,V>[] nt;
					// 如果当前线程无法协助进行数据转移, 则退出
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                   // 协助数据转移, 把正在执行transfer任务的线程数加1
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                 // sc置为负数, 当前线程自身成为第一个执行transfer(数据转移)的线程
            // 这个CAS操作可以保证,仅有一个线程会执行扩容
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
            }
        }
    }

扩容分为以下两种情况
1、已经有其它线程正在执行扩容了,则当前线程会尝试协助“数据迁移”;(多线程并发)
2、没有其它线程正在执行扩容,则当前线程自身发起扩容。(单线程)

注意:这两种情况都是调用了transfer方法,通过第二个入参nextTab进行区分(nextTab表示扩容后的新table数组,如果为null,表示首次发起扩容)。
第二种情况下,是通过CAS和移位运算来保证仅有一个线程能发起扩容。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值