Day298.并发容器&并发队列 -Juc
Posted 阿昌喜欢吃黄桃
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day298.并发容器&并发队列 -Juc相关的知识,希望对你有一定的参考价值。
并发容器&并发队列
一、并发容器概览
二、集合类历史—迭代过程
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));
}
}
- 源码: 以get()方法为例
get()方法直接被sychronized修饰
,那么如果多线程执行,就只能有一个线程进入这个方法
②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("三月"));
}
}
- 源码: 以put()为例
同样使用sychronized修饰
,线程安全,但是只能有一个线程使用这个方法
2、ArrayList 和 HashMap
虽然这两不是线程安全的,但是可以通过:使他们变成线程安全的
- Collections.sychronizedList(new ArrayList< E >())
- 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接口
ArrayList是实现了RandomAccess接口的。—随机访问接口
RandomAccess就是这个集合能否跳着被访问
那上面他就会进入new SynchronizedRandomAccessList<>(list)这部分
↓↓↓,他继承了SynchronizedList
那么进入SynchronizedList中,发现他里面方法的实现都是通过sychronized同步代码块
实现
总结:
通过Collections.sychronizedList等方式实现将ArrayList&HashMap转为线程安全的方法不比上面的Vector&Hashtable高明多少,从同步方法 -->同步代码块
3、ConcurrentHashMap 和 CopyOnWriteArrayList
-
取代同步的HashMap 和 ArrayList(时代巨轮滚滚向前)
-
绝大多数
并发情况下,ConcurrentHashMap 和 CopyOnWriteArrayList的性能都很好 -
他们更适合
读多写少
的场景 -
当经常有
写操作
,那么通过Collections.sychronizedList等转换为线程安全的情况,性能就比ConcurrentHashMap 好
三、ConcureentHashMap
在学习ConcureentHashMap前,需要对MAP进行回顾
1、Map接口
①Map接口的实现&继承
②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
}
}
2、为什么需要ConcurrentHashMap
①为什么不用上面的Collections.sychronizedMap()???
Collections.sychronizedMap通过一个锁来保证线程安全;
但是sychronized在高并发的情况下性能不好,所以不采用
②为什么HashMap线程不安全???
死循环造成CPU使用率100%
在多个线程同时扩容的时候,会造成链表的死循环(你指向我,我指向你)
- 同时put
碰撞
导致数据丢失
如果两个数据计算出hash值后算出对应集合位置后,如果2个数据计算出的位置重复,那肯定会丢失1个数据
- 同时put
扩容
导致数据丢失
多个线程同时put,发现同时需要扩容,那只会保留一个扩容后的数组
3、HashMap1.7&1.8结构特点
拉链法
- 1.7
- 1.8
- HashMap并发特点
- 非线程安全
- 迭代时不允许修改内容
- 只读的并发是安全的
- 如果一定要使用HashMap在并发环境,那请用
Collections.sychronizedMap()
;
4、ConcurrentHashMap1.7&1.8结构
- 1.7
最开始默认16个Segment,Segment类似块,每个Segment里面对应一个类似HashMap的数组+链表的数据结构
- 1.8
5、get/put()方法分析
-
put()
- 工作流程
它调用putVal()方法
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()
- 工作流程
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个节点就要转成红黑树呢?
作者其实在源码的注释中已经做了解释
原因是:
在数据量不多的情况下,用链表也无所谓,再慢,也无非执行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++的那个案例
那为什么呢?
因为他只能保证一个get()、一个put()操作是具有线程安全的,但不能保证这一个一系列的组合操作线程安全
还通过sychronized
来保证这个组合操作的线程安全,但是不推荐
,那都可以直接使用hashmap了
- 推荐方式: 使用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、诞生的原因
2、使用场景
读操作很多,且很快;但是写操作慢点没事
3、读写规则
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并发容器非阻塞队列的并发容器