##并发容器精讲
并发容器概览
集合类的历史
进入查看它的方法:
发现了synchronized的同步方法,
然后,会发现他的很多方法都是synchronized修饰的
由于有多个同步方法,而同步方法是不能由多个线程同时执行的,所以说,他的性能不会很安全。
下面经常查看Hashtable,发现情况是一样的。
所以,Hashtable效率不是很高。
升级版
代码演示:
package collections.predecessor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 描述: 演示Collections.synchronizedList(new ArrayList<E>())
*/
public class SynList {
public static void main(String[] args) {
// 它与普通的ArrayList用法是很小的
List<Integer> list = Collections.synchronizedList(new ArrayList<>());
list.add(5);
System.out.println(list.get(0));
}
}
源码分析:
它是符合RandomAccess的。所以返回的是SynchronizedRandomAccessList这个类。
进来以后发现他是继承了SynchronizedList
再进入以后,发现他用的是同步代码块的形式
所以,虽然,他能保证安全,其实他的性能并没有多大的提高。
下面进入到比较不错的实现:
ConcurrentHashMap基本上都是比以前Hashtable的好,CopyOnWriteArrayList它基本上也是比以前同步的ArrayList好的。除了写特别多的情况下。
ConcurrentHashMap
map:根据key所计算出来的hashCode来存储值。
HashMap死循环是在并发的过程中,导致了环形链表。
java7中
如果两个数的hashCode相同,那么它就会根据链表来进行保存。
到了java8,如果同一个k对应的hashCode数量不多的话,那么就拉链法,但是,当多到一定程度时,就会转为红黑树。
红黑树是对二叉查找树的一种平衡策略。二叉平衡树是左边结点的值小于根节点,根节点小于右边结点的值。但是二叉平衡树呢,可能会深度很深,红黑树会对其进行平衡,防止极端不平衡的发生。
我个人理解,读反正是从16个里面进行读,读是没有线程安全的,只要,没有进行写,就可以读。
而写的话,可以在16个segment中进行写。假设让它一个segement对应一个map,这样的话,读的话可能需要从16个里面进行分别进行读了。
采用的是一个一个node,node中元素大于8(默认值)时,会将这个链表转为红黑树。把查询时,复杂度从o(n)降成了o(logn)
对ConcurrentHashMap 的 put 和 get 方法进行分析
ConcurrrentHashMap和HashMap不同,不允许key和value为空。
下面对源码进行分析:
这里逻辑是,先判断tab(这里存的是map中,key-value的所有键值对) 即key-value是否初始化,如果没初始化,那就进行初始化操作。
如果,hash值发现为null,即这个key的hashcode,在hash表中是null,那说明就找到了。就用cas的方式来进行加入。放进去以后就break了,因为,已经把放进去了。如果没找到,也会break,这个就是cas的原理。
如果不行的话,就找到这里,看一下它当前的hash值是不是为MOVED状态,MOVED状态代表着是否为扩容状态,如果是扩容状态的话,就帮助他进行扩容。
接下来是判断当前hashcode等不等于key如果等于key的话,把值赋给oldVal,等会返回。
如果结点没有的话,就新建一个节点,把这个节点放到链表最后。
如果,在往下走,就已经到红黑树了
这里重要的是putTreeVal 把想要的值放入。
如果走到这里,说明值已经加入了,这个值是8,所以最低需要8个才会转成红黑树。
除了要满足大于等于8的条件,还需要小于MIN_TREEIFY_CAPACITY = 64;如果都满足的话,就会把链表转为红黑树。
以上就是put方法完成的内容,它的任务就是将值放在ConcurrenetHashMap中。
下面进行ConcurrentHashMap.get()方法的分析:
首先,他将算出key的hash值,用h来表示;
然后他去判断,tab不为null并且长度大于零,如果结果为false,说明没有初始化完毕,直接返回null;
如果为true,就说明是去寻找的过程。
如果槽点的hash值符合,并且key也是符合的话,说明找到了,就返回value;
如果得到的hash值是负数,说明是一个红黑树结点,就用find这个方法去找。
如果,第一个结点不是,而且也不是红黑树,那就说明是链表,就用链表的方法去寻找。
第一个问题:1.7数据结构是16个segment,每一个segment中下面,在用hash表,如果有两个对象返回的hash值相同的话,就会以链表的形式进行保存。
而1.8中,数据结构是用hash表+链表,当有多个对象的hash值相同时,如果对象数大于8,就转化为红黑树结构,如果小于等于8,就用链表结构。
由于数据结构不同,其并发性也不一样,1.7最多只能支持16个线程,起采用保证线程安全的方式是Reentrantlock锁的结构。
而1.8采用的是synchronized 和cas的方式,而且,他的结构保证了hash表中有几个,就可以支持几个。并发数更多。
Hash碰撞 在1.7中,采用的是拉链法,在1.8中,如果是小于等于8,采用的是链表,大于8把其转换为红黑树。
并发安全,查看第一点
查询复杂度,1.7 是链表,复杂度是O(n); 1.8中如果变成了红黑树,复杂度是O(logn);
为什么要超过8转为红黑树,
其实红黑树的优点是快,但是,存储空间是链表的两倍;是用时间换空间;
那么为什么要选择数字8呢?
因为在之前1-7,用链表虽然会慢一点,但是,由于数量少还是可以接受的。
而作者也对hash表中,链表数量有过统计
它发现当结点等于8时,概率小于千万分1。所以,它选择用8,主要是怕hash算法出现问题,导致对象hash值相同的问题产生。可以确保在极端情况下,我们的查询还有一定的效率。
错误使用导致了,ConcurrentHashMap线程不安全。
package collections.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);
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
// 线程不安全原因是:这是组合操作,既有put,又有get
Integer score = scores.get("小明");
Integer newScore = score + 1;
// 它可以保证的是多个线程同时put,结果不会混乱。
scores.put("小明", newScore);
}
}
}
结果如下:
分析原因:这边对ConcurrentHashMap集合既有get,又有put操作。所以,这将导致失败。
ConcurrentHashMap只能保证,多个线程一起put的时候,不会有线程安全问题。
为了解决刚刚那个问题,我们将调用ConcurrentHashMap的replace方法。
package collections.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);
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
while (true) {
Integer score = scores.get("小明");
Integer newScore = score + 1;
// 会返回结果,结果为true或者false
// 当它去replace的时候,发现小明这个key对应的值确实是score,它就会原子性将它变成newScore
// 在执行replace的时候是线程安全的
boolean b = scores.replace("小明", score, newScore);
if (b){
break;
}
}
}
}
}
这样就可以了。结果为2000;
还有一个putIfAbsent
如果包含key那就取出来,如果不包含key那就加入。