【Beautiful JUC Part.8】ConcurrentHashMap等并发集合

【Beautiful JUC Part.8】ConcurrentHashMap等并发集合

并发容器概览、集合类的历史、ConcurrentHashMap(重点、面试常考)

CopyOnWriteArrayList、并发队列Queue(阻塞队列、非阻塞队列)

一、并发容器的概览

  • ConcurrentHashMap:线程安全的HashMap

  • CopyOnWriteArrayList:线程安全的List

  • BlockingQueue:这是一个接口,表示阻塞队列,非常适合用于作为数据共享的通道

  • ConcurrentLinkedQueue:高效的非阻塞并发队列,使用链表实现。可以看做一个线程安全的LinkedList

  • ConcurrentSkipListMap:是一个Map,使用跳表的数据结构进行快速查找

二、古老和过时的同步容器(历史)

1、Vector和Hashtable

JDK早起设计的并发安全的集合类,主要问题是性能不好。

vector的源码,大多数都是由synchronized保护的,性能不好。

hashtable也是一样的,大多数都是synchronized修饰的,高并发性能差

image-20220210101746165

2、HashMap和ArrayList

这俩是线程不安全的,但是有升级版

image-20220210101927146

下面是具体的实现代码

image-20220210102239241

可以看出和声明vector和hashtable的大同小异。所以性能也一般。

3、ConcurrentHashMap和CopyOnWriteArrayList

取代同步的HashMap和同步的ArrayList

绝大多数并发情况下,ConcurrentHashMap和CopyOnWriteArrayList的性能都更好

三、ConcurrentHashMap

Map简介、为什么需要ConcurrentHashMap、HashMap的分析

JDK1.7中的ConcurrentHashMap的实现和分析

JDK1.8中的ConcurrentHashMap实现和源码分析

对比两个版本优缺点

组合操作:ConcurrentHashMap也不是线程安全的?

1、Map简介

HashMap、Hashtable、LinkedHashMap、TreeMap都是这个接口的实现

2、为什么需要ConcurrentHashMap?

HashMap是线程不安全的

同时put碰撞导致数据丢失

同时put扩容导致数据丢失

死循环造成的CPU100%(仅在JDK7及以前存在)

  • 多个线程扩容单的时候,会造成链表的死循环(你指向我,我指向你)
    • 其实这本来就不是个问题,因为HashMap本身就是不支持并发的,要并发就ConcurrentHashMap

3、HashMap并发的特点

非线程安全、迭代时候不允许修改内容、只读的并发是安全的,如果一定要把HashMap用在并发环境,用Collections.synchronizedMap(new HashMap())

4、JDK1.7的ConcurrentHashMap实现和分析

JDK1.7的ConcurrentHashMap实现和分析 1

JDK1.7的ConcurrentHashMap实现和分析 2

Java7中的ConcurrentHashMap最外层是多个segment,每个segment的底层数据结构与HashMap类似,仍然是数组和链表组成的拉链法、

每个segement独立上ReentrantLock锁,每个segment之间互不影响,提高了并发效率。

ConcurrentHashMap默认有16个Segments,所以最多可以同时支持16个线程并发写(操作分别分布在不同的Segment上)。这个恶魔人之可以再初始化的时候设置为其他值,但是一旦初始化以后,是不可以扩容的。

5、JDK1.8的ConcurrentHashMap实现和分析

image-20220210105340418

红黑树把查询从O(n)变为了O(logn)

putVal的流程

image-20220210110044608

get的流程

image-20220210110113961

6、为什么要把1.7的结构改成1.8的结构

数据结构

Hash碰撞

保证并发安全

  • 1.7是采用的分段锁,segment来保证并发安全,segment继承自ReentrantLock
  • 1.8是通过CAS加上synchronized

查询复杂度提高了

为什么超过8要转为红黑树?

image-20220210110609787

默认是链表,想要达到冲突为8,正常情况下是比较难的,概率只有千万分之一,如果真的发生这样的情况,可以确保在极端情况下,采用红黑树占用较大空间,提高查询效率。

7、COncurrentHashMap也不是线程安全的?

组合操作线程不安全

import java.util.concurrent.ConcurrentHashMap;

/**
 * 描述: 组合操作并不保证线程安全
 */
public class OptionsNotSafe implements Runnable{
    private static ConcurrentHashMap<String, Integer> scores = new ConcurrentHashMap<String, Integer>();

    public static void main(String[] args) throws InterruptedException {
        scores.put("小明", 0);
        Thread t1 = new Thread(new OptionsNotSafe());
        Thread t2 = new Thread(new OptionsNotSafe());
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(scores.get("小明"));
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            Integer score = scores.get("小明");
            Integer newScore = score + 1;
            scores.put("小明", newScore);
        }
    }
}

image-20220210122718178

ConcurrentHashMap可以保证单个操作,比如说单个put或者get操作是线程安全的,但是组合操作,并不保证。

解决办法

方法一:上锁

image-20220210122914156

这种方法不好,和普通的HashMap没啥区别了

方法二:使用replace方法

image-20220210123143186

运行结果

image-20220210123201914

方法二演进:

image-20220210123249755

image-20220210123256542

四、CopyOnWriteArrayList

1、诞生历史

image-20220210123903198

2、使用场景

读操作可以尽可能的快,而写即是慢一些也没有太大关系

读多写少:黑名单,每日更新;监听器:跌倒操作远多于修改操作

3、读写规则

image-20220210124147116

4、普通ArrayList缺陷

package collections.copyonwirte;

import java.util.ArrayList;
import java.util.Iterator;

/**
 * 描述:演示CopyOnWriteArrayList可以再迭代的过程中修改数组内容
 * 但是ArrayList不行
 */
public class CopyOnWriteArrayListDemo1 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();

        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("5");

        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            System.out.println("list is" + list);
            String next = iterator.next();
            System.out.println(next);

            if (next.equals("2")) {
                list.remove("5");
            }
            if (next.equals("3")) {
                list.add("3 found");
            }
        }
    }
}

缺陷就是不能再迭代的时候进行修改

image-20220210124642928

5、CopyOnWriteArrayList应用

将4中的代码改变

image-20220210124913459

运行结果

image-20220210124929121

为什么迭代器最后还是5呢?这就是它的特性,在迭代过程中,你改你的内容,我按照原来的去执行。

6、实现原理

CopyOnWrite的含义

意思就是在写的时候,将原来的数据copy一份到新的内存中去,然后再新的内存中做写操作,这样在修改完之后,再把指针指向新的内存区域,这样就实现了线程安全。并且CopyOnWrite的适用场景大多是读情况比较高,写操作较少。

总结就是创建新的副本、读写分离、旧的容器是不可变的。

迭代器对比

import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * 描述:     对比两个迭代器
 */
public class CopyOnWriteArrayListDemo2 {

    public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(new Integer[]{1, 2, 3});

        System.out.println(list);

        Iterator<Integer> itr1 = list.iterator();

        list.remove(2);
        Thread.sleep(1000);
        System.out.println(list);

        Iterator<Integer> itr2 = list.iterator();

        itr1.forEachRemaining(System.out::println);
        itr2.forEachRemaining(System.out::println);

    }
}

image-20220210125711132

可以看出迭代器和生成这个迭代器时候的状态有关,并不是实时变化的。

源码分析

image-20220210125919220

是一个array数组,上锁时候用的ReentrantLock()

image-20220210130036989

上面的添加操作,是上锁的,我们看一下get()方法

image-20220210130203894

整个get方法是没有上锁的,读操作不会出现阻塞。

7、缺点

image-20220210125827064

五、并发队列

1、为什么使用队列

队列可以在线程间传递数据:生产者消费者模式、银行转账

考虑线程安全的重任转移到了“队列”上

image-20220210130934784

2、阻塞队列BlockingQueue

什么是阻塞队列

image-20220210131349906

image-20220210131257662

阻塞功能:最具特色的两个带有阻塞功能的方法

  • take()方法:获取并移除队列的头结点,一旦如果执行take的时候,队列里无数据,则阻塞,直到队列里有数据

  • put()方法:插入元素。但是如果队列已满,那么就无法继续插入,则阻塞,直到队列里有了空闲空间

  • 是否有界(容量有多大):这是一个非常重要的属性,无界队列意味着里面可以容纳非常多(Integer.MAX_VALUE,约为2的31次,是一个非常大的数,可以认为是无限容量)

  • 阻塞队列和线程池的关系:阻塞队列是线程池的重要组成部分

BlcokingQueue的主要方法

image-20220210131803427

3、重要实现ArrayBlockingQueue

使用案例

有10个面试者,一共只有1个面试官,大厅里有3个位子供面试者休息,每个人的面试时间是10秒,模拟所有人面试的场景

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
 * 描述:     TODO
 */
public class ArrayBlockingQueueDemo {


    public static void main(String[] args) {

        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);

        Interviewer r1 = new Interviewer(queue);
        Consumer r2 = new Consumer(queue);
        new Thread(r1).start();
        new Thread(r2).start();
    }
}

class Interviewer implements Runnable {

    BlockingQueue<String> queue;

    public Interviewer(BlockingQueue queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        System.out.println("10个候选人都来啦");
        for (int i = 0; i < 10; i++) {
            String candidate = "Candidate" + i;
            try {
                queue.put(candidate);
                System.out.println("安排好了" + candidate);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        try {
            queue.put("stop");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class Consumer implements Runnable {

    BlockingQueue<String> queue;

    public Consumer(BlockingQueue queue) {

        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String msg;
        try {
            while(!(msg = queue.take()).equals("stop")){
                System.out.println(msg + "到了");
            }
            System.out.println("所有候选人都结束了");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

源码分析

image-20220210132636186

4、LinkedBlockingQueue

无界、容量为Integer.MAX_VALUE、内部结构:Node、两把锁。

构造方法

image-20220210132852636

内部属性:有两把锁,take和put锁

image-20220210132833450

put方法

image-20220210133019713

如果已经满了,就休息,如果没有满,就把这个结点放进去队列。

5、其他阻塞队列

PriorityBlockingQueue

支持优先级

自然顺序(而不是先进先出)

无界队列

PriorityQueue的线程安全版本

SynchronusQueue

image-20220210133248629

image-20220210133255272

image-20220210133308536

DelayQueue

image-20220210133332727

6、非阻塞并发队列

image-20220210133431992

7、如何选择适合自己的队列

考虑边界、空间、吞吐量

posted @ 2022-02-10 13:37  DarkerG  阅读(53)  评论(0编辑  收藏  举报