Java并发工具类(JUC)汇总(二)集合


二、集合

1.BlockingQueue

1.1 简介


 BlockingQueue是一个接口,用于实现带阻塞功能的队列,类似于消费者生产者阻塞队列,队列中按照先进先出(FIFO)的原则对元素进行排序。

在这里插入图片描述
 

1.2 注意事项


 BlockingQueue有如下注意事项:

  • BlockingQueue不接受空元素
    实现在尝试添加、放置或提供null时抛出NullPointerException
  • BlockingQueue可能是容量有限的
    允许设置容量,在未超出容量时,可以无阻塞的防止元素,不设置容量时,容量最大值为Integer.MAX_VALUE(无界队列)
  • BlockingQueue继承自Collection接口,使用Collection提供的批量操作的方法非线程安全
    例如addAll、containsAll、retainAll和removeAll不一定以原子方式执行

 

1.3 方法介绍


1.3.1 插入

	/**
     * 插入队列
     * 适用:有界队列
     * @param e 队列元素
     * @return 插入是否成功
     * @throws IllegalStateException 没有可用空间
     */
    boolean add(E e);


    /**
     * 插入队列
     * 适用:无界队列
     * @param e 队列元素
     * @return 插入是否成功
     */
    boolean offer(E e);

    /**
     * 插入队列,空间满时等待,一直阻塞到超时为止
     * @param e 队列元素
     * @param timeout 阻塞超时时间
     * @param unit 等待时间单位
     * @return 插入是否成功
     * @throws InterruptedException 中断异常
     */
    boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;


    /**
     * 插入队列,空间满时等待,一直阻塞到有空间为止
     * @param e 队列元素
     * @throws InterruptedException 中断异常
     */
    void put(E e) throws InterruptedException;

 

1.3.2 删除
	/**
     * 获取并删除队列头元素,队列中没有元素时等待,一直阻塞到有元素为止
     * @return 队列头元素
     * @throws InterruptedException 中断异常
     */
    E take() throws InterruptedException;


    /**
     * 取并删除队列头元素,队列中没有元素时,返回null
     * @return
     */
    E poll();


    /**
     * 获取并删除队列头元素,队列中没有元素时等待,一直阻塞到超时为止
     * @param timeout 阻塞超时时间
     * @param unit 阻塞超时时间单位
     * @return
     * @throws InterruptedException 中断异常
     */
    E poll(long timeout, TimeUnit unit) throws InterruptedException;



    /**
     * 如果存在该元素,删除,存在多个,只删除一个
     * @param o 被删除的元素
     * @return 是否删除成功
     */
    boolean remove(Object o);

 

1.3.3 查看
 
 	/**
     * 检索但不删除此队列的头部。此方法与peek的唯一区别在于,如果队列为空,则抛出异常。
     * @throws NoSuchElementException – 队列空
     * @return 队列的头部
     */
    E element();

    /**
     * 检索但不删除该队列的头部,如果该队列为空则返回null
     * @return 队列的头部或Null
     */
    E peek();

 

1.3.4 其他

	/**
     * 队列中是否包含某元素
     * @param o 查询的元素
     * @return 队列中是否包含某元素
     */
    public boolean contains(Object o);


    /**
     * 获取剩余容量
     * @return 剩余容量
     */
    int remainingCapacity();




    /**
     * 从该队列中删除所有可用的元素,并将它们添加到给定的集合中,此操作可能比重复轮询该队列更有效
     * @param c 待赋值的集合
     * @throws UnsupportedOperationException 如果指定的集合不支持添加元素
     * @throws IllegalArgumentException 如果指定的集合是这个队列,或者这个队列的某个元素的属性阻止它被添加到指定的集合
     * @return
     */
    int drainTo(Collection<? super E> c);


    /**
     * 最多从该队列中删除给定数量的可用元素,并将它们添加到给定的集合中
     * @param c 待赋值的集合
     * @param maxElements 最多删除并赋值的数量
     * @throws UnsupportedOperationException 如果指定的集合不支持添加元素
     * @throws IllegalArgumentException 如果指定的集合是这个队列,或者这个队列的某个元素的属性阻止它被添加到指定的集合
     * @return
     */
    int drainTo(Collection<? super E> c, int maxElements);

1.3.5 对比

 BlockingQueue的方法可以划分为如下四种类型,对应类型的方法如下表所示

  • 抛异常:
    如果试图的操作无法立即执行,抛一个异常。
  • 特定值:
    如果试图的操作无法立即执行,返回一个特定的值(常常是true/false)。
  • 阻塞:
    如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
  • 超时
    如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(典型的是true / false)。

 

抛异常特定值阻塞超时
插入add(e)offer(e)put(e)offer(e, time, unit)
删除remove(o)poll()take()poll(time, unit)
查询element()peek()

 

1.4 子类介绍

1.4.1 继承类图

 如下图所示,BlockingQueue的子类如下

 
在这里插入图片描述

 

1.4.2 ArrayBlockingQueue

 ArrayBlockingQueue是基于数组的有界队列,在初始化时,必须指定容量,支持公平锁和非公平锁,其使用方式如下:


//可支持公平或非公平锁
BlockingQueue<String> fair = new ArrayBlockingQueue<>(50,false);
BlockingQueue<String> queue = new ArrayBlockingQueue<>(50);
queue.put("1");
System.out.println(queue.take());

 

1.4.3 LinkedBlockingQueue

 LinkedBlockingQueue是一种基于单向链表的有界队列。因为队头和队尾是2个指针分开操作的,所以用了2把锁+2个条件,同时有1个AtomicInteger的原子变量记录count数,意味着put和put互斥,take和take互斥,put和take不互斥,其使用方式如下:


//不指定容量,容量为Integer.MAX_VALUE
BlockingQueue<String> unbounded = new LinkedBlockingQueue<String>();
//指定容量
BlockingQueue<String> bounded   = new LinkedBlockingQueue<String>(50);
bounded.put("1");
System.out.println(bounded.take());

 

1.4.4 PriorityBlockingQueue

 PriorityBlockingQueue是一个按照优先级从小到大出列的无界队列,插入元素必须实现 java.lang.Comparable 接口,优先级相同的元素无法保证顺序,Iterator遍历不是按照优先级排序


BlockingQueue<String> queue   = new PriorityBlockingQueue<>(5);
//String本身就实现了Comparable接口
queue.put("2");
queue.put("1");
System.out.println(queue.take());
System.out.println(queue.take());

 

1.4.5 DelayQueue

 DelayQueue是一个按延迟时间从小到大出列的无界队列,入队的元素必须实现Delayed接口, 除队列满或空外,未到延时期也会造成阻塞,可用于定时任务调度的实现



public static class MyDelayData implements Delayed {


   /**
    * 数据
    */
   private final String data;

   /**
    * 剩余生存时间
    */
   private final long availableTime;

   public String getData() {
       return data;
   }

   public long getAvailableTime() {
       return availableTime;
   }

   /**
    * 初始化
    * @param data 数据
    * @param delayTime 延迟时间
    */
   public MyDelayData(String data, long delayTime) {
       this.data = data;
       this.availableTime = delayTime + System.currentTimeMillis();
   }


   /**
    * 还剩多少时间到期 小于0到期
    * @param unit 时间单位枚举
    * @return
    */
   @Override
   public long getDelay(TimeUnit unit) {
       return unit.convert(availableTime - System.currentTimeMillis(),TimeUnit.MILLISECONDS);
   }

   /**
    * 排序
    * @param o
    * @return
    */
   @Override
   public int compareTo(Delayed o) {
       MyDelayData others = (MyDelayData) o;
       return (int)(this.availableTime - others.getAvailableTime());
   }
}


public static void main(String[] args) throws InterruptedException {
    BlockingQueue<MyDelayData> queue = new DelayQueue<>();
    queue.put(new MyDelayData("剩余时间为50ms的数据",50));
    queue.put(new MyDelayData("剩余时间为100ms的数据",100));
    System.out.println(queue.take().getData());
    System.out.println(queue.take().getData());
}

 

1.4.6 SynchronousQueue

 SynchronousQueue是一个不存储元素的无界阻塞队列,生产和消费必须一一对应才不会阻塞,如先线程1调用put后不存在任意一个线程调用take,故阻塞,线程2调用take后,线程1才被唤醒,也可以多个线程同时调用put或take,所有调用的线程都被阻塞,直到有其他线程调用take或put匹配后才唤醒


public static void main(String[] args) throws InterruptedException {
     //可传入公平或非公平
     BlockingQueue<String> fair = new SynchronousQueue<>(true);
     BlockingQueue<String> queue = new SynchronousQueue<>();

     new Thread(new Runnable() {
         @Override
         public void run() {
             try {
                 System.out.println("线程"+Thread.currentThread()+"放入元素前");
                 queue.put("1");
                 System.out.println("线程"+Thread.currentThread()+"放入元素后");
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     }).start();

     System.out.println("主线程获取"+queue.take());
 }

 

1.4.7 LinkedTransferQueue

 LinkedTransferQueue是一个链表组成的无界阻塞队列,也需要消费与生产一一匹配,可以理解为 SynchronousQueue +LinkedBlockingQueue ,性能比 LinkedBlockingQueue 更高(没有锁操作),比 SynchronousQueue能存储更多的元素。比其他队列多出在TransferQueue中定义的方法,如tryTransfer和transfer。


public static void main(String[] args) throws InterruptedException {
    TransferQueue<String> queue = new LinkedTransferQueue<>();

    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                String data = "元素1";
                //是否有消费者接收,如果有立即放入元素,没有什么都不做
                if (queue.tryTransfer(data)) {
                    System.out.println("已放入元素");
                }else {
                    System.out.println("尝试阻塞放入前");
                    //阻塞式放入,没有消费者就阻塞
                    queue.transfer(data);
                    System.out.println("尝试阻塞放入后");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();

    Thread.sleep(1000);
    System.out.println("消费者开始消费"+queue.take());
}


 

1.4.8 BlockingDeque

 BlockingDeque继承自BlockingQueue,是一个带阻塞的双端队列,即头部和尾部都可以进行元素添加或删除操作,通常在线程即是消费者又是生产者的情况下使用(工作窃取队列),拥有BlockingQueue的功能的同时,又扩展了一部分接口,xxxFirst表示从队列头进行操作,xxxLast表示从队列尾进行操作,如下表所示

 

抛异常特定值阻塞超时
插入addFirst(e)offerFirst(e)putFirst(e)offerFirst(e, time, unit)
删除removeFirst(o)pollFirst()takeFirst()pollFirst(time, unit)
查询getFirst()peekFirst()

 

抛异常特定值阻塞超时
插入addLast(e)offerLast(e)putLast(e)offerLast(e, time, unit)
删除removeLast(o)pollLast()takeLast()pollLast(time, unit)
查询getLast()peekLast()

 

1.4.9 LinkedBlockingDeque

 LinkedBlockingDeque是由双向列表组成的阻塞队列,队列头部和尾部都可以添加和移除元素,多线程并发时,可以将锁的竞争最多降到一半。


BlockingDeque<String> queue = new LinkedBlockingDeque<>();
queue.putFirst("1");
queue.putLast("2");
System.out.println(queue.takeFirst());
System.out.println(queue.takeLast());

 

1.4.10 对比

 BlockingQueue子类对比如下表,在使用无界队列时,需要避免OOM的隐患

子类初始化容量公平性数据结构有界性
ArrayBlockingQueue构造函数设置构造函数设置数组有界
LinkedBlockingQueue构造函数设置,未设置时为Integer.MAX_VALUE非公平单链表有界
PriorityBlockingQueue构造函数设置,未设置时为11非公平数组实现的二叉小根堆无界(扩容)
DelayQueue11非公平PriorityQueue(数组实现的二叉小根堆)无界(扩容)
SynchronousQueue无容量,不存储元素构造函数设置公平TransferQueue,非公平TransferStack,底层都为单链表无界
LinkedTransferQueue无初始容量公平单链表无界
LinkedBlockingDeque构造函数设置,未设置时为Integer.MAX_VALUE非公平双向链表有界

 

2.CopyOnWriteArrayList/Set

2.1 简介


 CopyOnWriteArrayList是基于数组实现CopyOnWrite(写时复制,即读完全无锁,写时拷贝新数组进行写入,写入完成后再替换原数组的技术),同样实现该技术的还有CopyOnWriteArraySet,其内部使用CopyOnWriteArrayList并在此基础上封装了去重的操作,故本节重点讨论CopyOnWriteArrayList。

 

2.2 fail-fast

2.2.1 定义

fail-fast 机制,即快速失败机制,是java集合(Collection)中的一种错误检测机制
 当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast,即抛出 ConcurrentModificationException异常。

 

2.2.2 出现场景

2.2.2.1 单线程

 在常见的集合如ArrayList,HashMap等遍历的过程中,出现新增或删除等涉及到集合元素变化时就会触发fail-fast,但并不保证一定触发fail-fast,如下面示例的先增加再删除

//添加元素抛ConcurrentModificationException异常
public static void main(String[] args) {
    List<String> arrayList = new ArrayList<>();
    arrayList.add("1");
    for (String s : arrayList) {
        //添加元素
        arrayList.add("1");
    }
}

//删除元素抛ConcurrentModificationException异常
public static void main(String[] args) {
    List<String> arrayList = new ArrayList<>();
    arrayList.add("1");
    for (String s : arrayList) {
        //删除元素
        arrayList.remove(s);
    }
}

//修改元素不抛异常
public static void main(String[] args) {
    List<String> arrayList = new ArrayList<>();
    arrayList.add("1");
    for (String s : arrayList) {
        //修改元素
        arrayList.set(0,"update");
    }
}
//先增加再删除 不抛异常
public static void main(String[] args) {
    List<String> arrayList = new ArrayList<>();
    arrayList.add("1");
    for (String s : arrayList) {
        //添加元素
        arrayList.add("1");
        //删除元素
        arrayList.remove(s);
    }
}

2.2.2.2 多线程

 在多线程的环境下,一个线程使用迭代器进行遍历,另一个线程同时进行增加或删除也会出现fail-fast


public static void main(String[] args) {
    Map<String, String> map = new HashMap<>();
    for (int i = 0; i < 10; i++) {
        map.put(i + "", i + "");
    }

    //一个线程读
    new Thread(new Runnable() {
        @Override
        public void run() {
            for (String key : map.keySet()) {
                System.out.println(map.get(key));
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();

    //一个线程删
    new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                map.remove(i+"");
            }

        }
    }).start();


    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println(map);

}

 运行结果如下

0
1
Exception in thread "Thread-0" java.util.ConcurrentModificationException
	at java.util.HashMap$HashIterator.nextNode(HashMap.java:1445)
	at java.util.HashMap$KeyIterator.next(HashMap.java:1469)
{}

 

2.2.3 原理分析

 首先定位异常抛出的地方at java.util.HashMap$HashIterator.nextNode(HashMap.java:1445)

 if (modCount != expectedModCount)
                throw new ConcurrentModificationException();

 可以看出modCount != expectedModCount是异常出现的根本原因,expectedModCount是构造器初始化时就定义好不会改变的值,modCount 随着集合的元素的变化,如新增或删除或清除集合等,会发生改变,故对元素结构变化时会引发ConcurrentModificationException异常

2.2.3 避免fail-fast

 可以采用如下方式避免

  • 使用Iterator提供的操作方法
    如add,remove方法不会引起modCount的变化

  • 使用并发容器
    如CopyOnWriterArrayList、ConcurrentHashMap

2.3 实现原理

2.2.1 写入

 add 处理流程为,先复制一个数组,长度为原长度+1,然后把新值存放,最后赋值给原地址,set与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);
        // 存放元素e
        newElements[len] = e;
        // 赋值给原地址
        setArray(newElements);
        return true;
    } finally {
        // 释放锁
        lock.unlock();
    }
}
  

 remove处理流程为,计算删除的元素所在索引,分两段复制到同一个数组,索引前的和索引后的,不复制索引所在位置



public E remove(int index) {
    // 可重入锁
    final ReentrantLock lock = this.lock;
    // 获取锁
    lock.lock();
    try {
        // 获取数组
        Object[] elements = getArray();
        // 数组长度
        int len = elements.length;
        // 获取旧值
        E oldValue = get(elements, index);
        // 需要移动的元素个数
        int numMoved = len - index - 1;
        if (numMoved == 0) // 移动个数为0
            // 复制后设置数组
            setArray(Arrays.copyOf(elements, len - 1));
        else { // 移动个数不为0
            // 新生数组
            Object[] newElements = new Object[len - 1];
            // 复制index索引之前的元素
            System.arraycopy(elements, 0, newElements, 0, index);
            // 复制index索引之后的元素
            System.arraycopy(elements, index + 1, newElements, index,
                                numMoved);
            // 设置索引
            setArray(newElements);
        }
        // 返回旧值
        return oldValue;
    } finally {
        // 释放锁
        lock.unlock();
    }
}
  
  

 

2.2.2 读取

 以get为例,所有读操作未进行加锁


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


public E get(int index) {
    return get(getArray(), index);
}

 

2.2.3 迭代

 内部使用COWIterator迭代器,该迭代器的特点是持有元素集合的快照,不会抛出ConcurrentModificationException异常,但查询到的快照数据不具有强一致性,可能与元素组数据不一致,同时也不支持迭代器内部提供的remove、set 和 add方法



static final class COWIterator<E> implements ListIterator<E> {
 
    // 快照
    private final Object[] snapshot;
   
    // 游标
    private int cursor;
    
    // 构造函数
    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }
 
 
    // 不支持remove操作
    public void remove() {
        throw new UnsupportedOperationException();
    }


    // 不支持set操作
    public void set(E e) {
        throw new UnsupportedOperationException();
    }

  
    // 不支持add操作
    public void add(E e) {
        throw new UnsupportedOperationException();
    }
}

 

2.4 缺陷


 CopyOnWriteArrayList有如下缺陷:

  • 内存消耗大
    每一次写入都会复制数组,导致内存消耗大,可能导致young gc或者full gc,只有在读多写少的情况下才适用

  • 数据不实时
    复制数组并写入会消耗一定时间,遍历或读取时获取到的不一定是最新的数据

 

3.ConcurrentLinkedQueue/Deque

3.1 简介


 ConcurrentLinkedQueue是基于CAS实现的单链表队列,ConcurrentLinkedDeque是基于CAS实现的双链表队列。由于使用了无锁,所以性能优于阻塞队列的实现,在并发量较大的情况可以考虑使用该队列,但存取数据的逻辑需要自行实现。

 

3.2 HOPS(延迟更新策略)


 HOPS是一种延迟更新策略,即对tail指针(入队)和head指针(出队)的CAS操作,每两个节点更新一次,在高并发的常见下,会提升一定的效率。

 

3.2.1 tail更新时机

 tail指向的下一个节点不为NULL时,cas更新tail指针,间隔一次更新

在这里插入图片描述

 

3.2.2 head更新时机

 head指向的下一个节点为NULL时,cas更新head指针,间隔一次更新,在获取队列的大小时,需要从第一个非空的节点开始计数,因为head指针的更新不及时

在这里插入图片描述

 

4.ConcurrentHashMap

4.1 JDK1.7

4.1.1 结构图

 如下图所示ConcurrentHashMap,由16个Segment组成的数组组成,一个Segment存放HashEntry数组,HashEntry数组中存放由HashEntry组成的链表,基于分段锁机制控制并发。

 

在这里插入图片描述
 

4.1.2 初始化

 ConcurrentHashMap初始化要点如下

  • Segment数组初始化为16,不可扩容
    ConcurrentHashMap采用分段锁实现线程安全,在进行操作时,并非对整个ConcurrentHashMap加锁,而是对16个Segment加锁,故并发度为16且不可变更,在初始化时会初始化Segment[0],其余Segment初始化在put时进行初始化,为避免并发使用cas进行设置。

  • initialCapacity、loadFactor、concurrencyLevel
    initialCapacity 初始化容量 默认值16
    loadFactor 负载因子 默认值0.75
    concurrencyLevel 并发度 默认值16
    这三个参数是计算HashEntry数组初始化长度及扩容的核心参数,可在构造方法中设置

  • HashEntry数组长度cap计算
    初始化数组长度cap,最小值为2(MIN_SEGMENT_TABLE_CAPACITY = 2),若计算出来初始化长度小于2,则修改为2,避免插入第一个元素就扩容
    计算公式为:initialCapacity / concurrencyLevel(向上取整)

  • HashEntry数组扩容阈值(threshold)计算
    扩容阈值计算公式为:cap * loadFactor ,如2*0.75=1.5,HashEntry数组大于1.5时进行扩容,及等于2时扩容。
    在设置initialCapacity 时可以采用如下方式进行估算:
    initialCapacity = cap(期望设置的容量) / loadFactor + 1

 

4.1.3 扩容

 ConcurrentHashMap扩容流程为,当数组长度超过扩容阈值时,新建长度为原数组两倍的数组,重新计算hash值对应的槽,设置完成后将新数组地址赋给原地址,该流程在JDK1.8节详细分析。

 

4.2 JDK1.8

4.2.1 结构图

 1.8更改了ConcurrentHashMap的结构,与HashMap类似,扩展了红黑树和链表的相互转化,锁加在Node数组中的每一个节点上,Node数组可以进行扩容,并发度取决于Node数组的长度。

在这里插入图片描述

 

4.2.2 初始化

 ConcurrentHashMap初始化要点如下

  • ConcurrentHashMap(int initialCapacity)容量cap计算方式
    cap = 向上取最近的 2 的 n 次方(1.5 * initialCapacity +1),如initialCapacity 为64,cap等于128

  • ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel)容量cap计算方式
    cap = 向上取最近的 2 的 n 次方((long)(1.0 + (long)initialCapacity / loadFactor))

 

4.2.3 put过程

4.2.3.1 Node数组初始化

 使用CAS设置sizeCtl变量为-1,设置成功的线程进行初始化工作,其他线程自旋等待直到初始化成功停止

 

4.2.3.2 Node[i]链表初始化

 初始化Node结点并放入Node数组中

 

4.2.3.3 扩容

 其他线程正在扩容,帮助其完成扩容

 

4.2.3.4 元素put

 使用synchronized对Node加锁,加锁后根据头结点判断属于红黑树还是链表(TreeBin继承Node),执行元素put操作。
 操作完成后累加当前链表或红黑树的元素个数(binCount),判断是否需要转为红黑树转化。
红黑树与链表相互转化的条件,如下所示:

  • binCount >= TREEIFY_THRESHOLD,tab.length < MIN_TREEIFY_CAPACITY转为红黑树
    TREEIFY_THRESHOLD (树转化阈值)= 8 ,MIN_TREEIFY_CAPACITY(最小转化数组容量) = 64
  • binCount <= UNTREEIFY_THRESHOLD
    UNTREEIFY_THRESHOLD(树退回链表阈值)= 6

 

4.2.4 扩容

4.2.4.1 sizeCtl

 sizeCtl可以理解为HashMap的状态变量,通过CAS进行设置,有如下几种情况

  • sizeCtl = cap,tab=null
    未初始之前的初始容量
  • sizeCtl = -1
    表示整个HashMap正在初始化
  • sizeCtl < -1
    表示整个HashMap正在扩容,在按步长进行分段扩容时,sizeCtl 会被设置为较大的负数,每一个线程扩容完毕,sizeCtl +1,全部扩容完成后设置为下次扩容的阈值
  • sizeCtl < 0
    表示整个HashMap正在初始化
  • sizeCtl = 下次扩容的阈值(数组长度 * 0.75)> 0
    表示扩容完成

 

4.2.4.2 扩容流程

 扩容时,将整个tab的从后到前,划分成tab.len (Node数组长度)/ stride(步长)个段,每一段可由不同的线程并发执行,用transferIndex(初始时为Node数组长度)记录当前执行的进度,每一个线程执行完毕后CAS操作transferIndex减去固定的步长,当transferIndex为0时扩容结束

在这里插入图片描述

  • stride值计算
    单核 = Node数组长度
    多核 =(Node数组长度>>>3)/CPU核数
  • 扩容时转发节点
    已扩容完成的Node在旧的hashMap中会变为ForwardingNode(转发节点),该节点记录新hashMap的值引用,避免获取到旧的引用
  • 数据迁移
    节点rehash满足如下规律:
     节点hashCode & 原数组长度 n = 0 ,节点为低位,新节点所在数组下标i = 原数组下标i
     节点hashCode & 原数组长度 n = 1 ,节点为高位,新节点所在数组下标i = 原数组下标i + 原数组长度n
    第一步:计算lastRun节点(从这个节点开始后面的所有节点全为低位或者全为高位的节点称为lastRun节点),将其之后的所有链表放到高位或低位链表中。
    第二步:从节点头开始到lastRun节点结束,依次将节点放入高位或低位链表中
    第三步:直接将高位链表放在新数组i+n位置,低位链表放在新数组i位置

在这里插入图片描述

图片转自:https://blog.csdn.net/ZOKEKAI/article/details/90051567

 

5.ConcurrentSkipListMap/Set

5.1 简介


 ConcurrentHashMap是key无序的线程安全的map,ConcurrentSkipListMap是key有序的线程安全的map,同样的TreeMap是基于红黑树实现的key有序的非线程安全的map,目前尚未找到如何实现高效无锁的树,故使用基于链表的跳跃表来实现。

  ConcurrentSkipListSet是对ConcurrentHashMap的封装,本节只介绍ConcurrentSkipListMap

 

5.2 结构图


 由于链表本身具有插入删除快但搜索慢的特点,为了提高搜索效率,为有序链表建立索引可以有效减低链表搜索的时间复杂度,通过判断元素是否在某个区间,逐层查找,直到找到目标元素为止,是一种空间换时间的算法。

在这里插入图片描述

5.2 实现原理


 在并发环境下,由于key无序从头部或尾部进行插入可以有效避免并发问题,但在有序链表中不可避免的需要在中间插入或删除元素,若存在两个线程同时进行插入或修改,就有可能导致插入到已经被删除元素的节点后面,为了避免这个问题,ConcurrentSkipListMap采用使用将value值赋null的方式标记已删除的元素,在执行查找、删除、添加时,都会对node的value值进行校验,若为空,执行真正的删除操作解决并发问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值