Day298.并发容器&并发队列 -Juc

Posted 阿昌喜欢吃黄桃

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day298.并发容器&并发队列 -Juc相关的知识,希望对你有一定的参考价值。

并发容器&并发队列

一、并发容器概览

image-20210614145845012

image-20210614145943339


二、集合类历史—迭代过程

1、Vector 和 Hashtable

并发性能差,主要是内部方法由sychronized修饰;

①Vector

可以理解为一个线程安全的ArrayList;

最主要的缺陷性能不好

  • 代码演示: 基本使用
/******
 @author 阿昌
 @create 2021-06-14 15:02
 *******
 *      演示Vector
 */
public class VectorDemo {
    public static void main(String[] args) {
        Vector<String> vector = new Vector<>();
        vector.add("achang");
        System.out.println(vector.get(0));
    }
}

image-20210614150433670

  • 源码: 以get()方法为例

get()方法直接被sychronized修饰,那么如果多线程执行,就只能有一个线程进入这个方法

image-20210614150604439


②Hashtable

可以理解为一个线程安全的HashMap;

最主要的缺陷也是性能不好

  • 代码演示
/******
 @author 阿昌
 @create 2021-06-14 15:08
 *******
 *      演示Hashtable
 */
public class HashtableDemo {
    public static void main(String[] args) {
        Hashtable<String, Object> hashtable = new Hashtable<>();
        hashtable.put("三月","achang");

        System.out.println(hashtable.get("三月"));
    }
}

image-20210614150939741

  • 源码: 以put()为例

同样使用sychronized修饰,线程安全,但是只能有一个线程使用这个方法

image-20210614151020688


2、ArrayList 和 HashMap

虽然这两不是线程安全的,但是可以通过:使他们变成线程安全的

  1. Collections.sychronizedList(new ArrayList< E >())
  2. Collections.sychronizedMap(new HashMap< K,V >())
  • 代码演示:以Collections.sychronizedList为例子
/******
 @author 阿昌
 @create 2021-06-14 15:15
 *******
 *      演示Collections.sychronizedList(new ArrayList<E>())
 */
public class SycList {
    public static void main(String[] args) {
        List<Integer> list = Collections.synchronizedList(new ArrayList<>());
        list.add(5);
        System.out.println(list.get(0));
    }
}

那为什么通过Collections工具类的包装,那就线程安全了呢???

  • 源码: synchronizedList()方法

先是判断传入的类是否实现了RandomAccess接口

image-20210614151959475

ArrayList是实现了RandomAccess接口的。—随机访问接口

RandomAccess就是这个集合能否跳着被访问

image-20210614152102369

那上面他就会进入new SynchronizedRandomAccessList<>(list)这部分

↓↓↓,他继承了SynchronizedList

image-20210614152848672

那么进入SynchronizedList中,发现他里面方法的实现都是通过sychronized同步代码块实现

image-20210614153008180

总结

通过Collections.sychronizedList等方式实现将ArrayList&HashMap转为线程安全的方法不比上面的Vector&Hashtable高明多少,从同步方法 -->同步代码块


3、ConcurrentHashMap 和 CopyOnWriteArrayList

  • 取代同步的HashMap 和 ArrayList(时代巨轮滚滚向前)

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

  • 他们更适合读多写少的场景

  • 当经常有写操作,那么通过Collections.sychronizedList等转换为线程安全的情况,性能就比ConcurrentHashMap 好


三、ConcureentHashMap

在学习ConcureentHashMap前,需要对MAP进行回顾

1、Map接口

①Map接口的实现&继承

image-20210614154437765


②Map常用方法

/******
 @author 阿昌
 @create 2021-06-14 15:53
 *******
 *      演示Map的基本用法
 */
public class MapDemo {
    public static void main(String[] args) {
        HashMap<String, String> map = new HashMap<>();
        System.out.println(map.isEmpty());//返回集合是否为空
        map.put("三月","achang");
        map.put("阿昌","3306");//存放K-V
        System.out.println(map.keySet());;//返回整个键的集合
        System.out.println(map.get("三月"));; //获取K 对应的V
        System.out.println(map.size());;//获取当前集合大小
        System.out.println(map.containsKey("阿昌"));//判断集合是否包含 此K
        map.remove("阿昌");//删除集合中的 K-V
    }
}

image-20210614160134979


2、为什么需要ConcurrentHashMap

①为什么不用上面的Collections.sychronizedMap()???

Collections.sychronizedMap通过一个锁来保证线程安全;

但是sychronized在高并发的情况下性能不好,所以不采用

②为什么HashMap线程不安全???

  • 死循环造成CPU使用率100%

在多个线程同时扩容的时候,会造成链表的死循环(你指向我,我指向你)

  • 同时put碰撞导致数据丢失

如果两个数据计算出hash值后算出对应集合位置后,如果2个数据计算出的位置重复,那肯定会丢失1个数据

  • 同时put扩容导致数据丢失

多个线程同时put,发现同时需要扩容,那只会保留一个扩容后的数组


3、HashMap1.7&1.8结构特点

拉链法

  • 1.7

image-20210614163308297

  • 1.8

image-20210614163646027


  • HashMap并发特点
  1. 非线程安全
  2. 迭代时不允许修改内容
  3. 只读的并发是安全的
  4. 如果一定要使用HashMap在并发环境,那请用Collections.sychronizedMap();

4、ConcurrentHashMap1.7&1.8结构

  • 1.7

image-20210614170604950

最开始默认16个Segment,Segment类似块,每个Segment里面对应一个类似HashMap的数组+链表的数据结构

image-20210614170630263

image-20210614170835062


  • 1.8

image-20210614170920638


5、get/put()方法分析

  • put()

    • 工作流程

image-20210614175334178

它调用putVal()方法

image-20210614171537833

putVal()方法:

/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    //这里和hashmap不一样,hashmap允许一个元素的key为null,但是这里就不允许了
    如果这个槽点没有值
    if (key == null || value == null) throw new NullPointerException();
    //计算出自己的hash值
    int hash = spread(key.hashCode());
    int binCount = 0;
    //在这个for循环中,完成对 值的插入工作
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        //判断tab是否没有被初始化,或长度等于0,他就进行初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //如果他已经被初始化,且这个位置是空的,那就直接放入赋值
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //CAS操作
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        //判断当前的Hash值是不是MOVED
        //MOVED代表一种特殊的节点,一种转移节点,说明这个槽点正在扩容
        else if ((fh = f.hash) == MOVED)
            //帮助进行扩容和转移工作
            tab = helpTransfer(tab, f);
        else {
            //如果这个槽点有值
            V oldVal = null;
            //保证线程安全
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        //进行链表操作,根据当前hash值,找到这个hash该放的对应链表位置
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //判断当前存在不存在这个hash对应的key
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                //把原来的oldVal赋成新值,并过会返回oldVal
                                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;
                            }
                        }
                    }
                    //判断他是否是一个红黑树
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        //putTreeVal()把值放到红黑树中
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                              value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            //走到这,代表已经完成添加操作了
            if (binCount != 0) {
                //判断是否要将链表转成红黑树
                //TREEIFY_THRESHOLD默认值为8,代表链表节点最少为8个才会尝试转成红黑树
                if (binCount >= TREEIFY_THRESHOLD)
                    //treeifyBin()转换红黑树方法
                    //这里方法会要求数组的长度要大于默认的64;
                    //且链表节点长度要大于等于8个节点才会转红黑树
                    treeifyBin(tab, i);
                if (oldVal != null)
                    //最后这里就是上面说的返回oldVal值
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

  • get()

    • 工作流程

    image-20210614175426150

image-20210614174835199

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    //获取到这个key的hash值,并用h来表示
    int h = spread(key.hashCode());
    //判断当前的这个数组长度不能等于null,且长度大于0,否则就直接返回null
    //代表这个map都没被建立初始化完毕
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        //如果这个key对应的hash赋值这个槽点的hash值
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                //就返回val,说明找到了
                return e.val;
        }
        //如果为负数,说明他是一个红黑树节点或者转移节点
        else if (eh < 0)
            //那就用find()方法去找到这个红黑树对应的位置
            return (p = e.find(h, key)) != null ? p.val : null;
        //这里就是,这个他又不是数组,又不是红黑树
        //那他这里就是一个链表数据结构
        //那就用while循环遍历这个链表
        while ((e = e.next) != null) {
            //找到对应的值
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                //返回
                return e.val;
        }
    }
    return null;
}

6、1.7结构和1.8结构的对比

为什么要把1.7的块结构改成1.8这样子类似hashmap的结构???

  • 数据结构不同
    • 1.7采用Segment块的结构,默认16个块,也就是16个线程数并发
    • 1.8采用类似hashmap的数组+链表+红黑树结构,不限制线程数
    • 并发度的改变从 16个------>不限
  • Hash碰撞
    • 1.7采用拉链法,链表的形式往下
    • 1.8采用拉链法,链表形式往下,然后在根据条件转成红黑树
  • 保证并发安全
    • 1.7采用分段锁,通过Segment块保证线程安全,Segment块继承ReentrantLock
    • 1.8采用unsafe工具类的CAS操作 + sychronized修饰符
  • 查询复杂度
    • 1.7链表查询复杂度为:O n
    • 1.8假设编程了红黑树:O logn

那为什么超过链表超过8个节点就要转成红黑树呢?

作者其实在源码的注释中已经做了解释

image-20210614180516976

原因是:

在数据量不多的情况下,用链表也无所谓,再慢,也无非执行7-8个链表;

那为什么后面要转呢?

因为红黑树每个节点所占用的空间是链表的两倍,空间损耗要比链表大

所以一开始采用默认占用空间更少的链表形式存取

但是实际的情况下,8次冲突,然后转成红黑树的情况只有千万之一:也就是上面的图0.00000006


7、在某种情况下ConcurrentHashMap也不是线程安全的

错误的使用会造成他线程不安全

  • 代码演示
/******
 @author 阿昌
 @create 2021-06-14 18:17
 *******
 *      组合操作并不保证线程安全
 */
public class OptionsNotSafe implements Runnable {
    private static ConcurrentHashMap<String,Integer> scores = new ConcurrentHashMap<>();

    public static void main(String[] args) throws InterruptedException {
        scores.put("阿昌",0);
        OptionsNotSafe r = new OptionsNotSafe();
        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println(scores);
    }

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

}

感觉回到了那个熟悉的a++的那个案例

image-20210614182705598

那为什么呢?

image-20210614183236268

因为他只能保证一个get()、一个put()操作是具有线程安全的,但不能保证这一个一系列的组合操作线程安全

还通过sychronized来保证这个组合操作的线程安全,但是不推荐,那都可以直接使用hashmap了

image-20210614183429662

image-20210614183511103

  • 推荐方式: 使用replace()方法,CAS操作
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
                while (true) {
                    Integer score = scores.get("阿昌");
                    Integer newScore = score + 1;
                    boolean flag = scores.replace("阿昌", score, newScore);
                    System.out.println(flag);
                    if (flag){break;}
                }
        }
    }

尽量避免使用组合操作,而是直接使用ConcurrentHashMap提供的方法来实现组合操作,并通过返回值的boolean来判断是否修改成功,不然就一直尝试修改


四、CopyOnWriteArrayList

1、诞生的原因

image-20210614185513077


2、使用场景

读操作很多,且很快;但是写操作慢点没事

image-20210614185751965


3、读写规则

CopyOnWriteArrayList只有写写互斥,读都不加锁

在写的过程中,也可读

image-20210614190028843

  • 代码演示

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并发容器非阻塞队列的并发容器

Day839.并发容器-Java 并发编程实战

Day839.并发容器-Java 并发编程实战

并发编程: 同步容器并发容器阻塞队列双端队列

并发容器 - 各种队列

并发队列