ConcurrentHashMap等并发集合

##并发容器精讲

在这里插入图片描述
在这里插入图片描述
并发容器概览
在这里插入图片描述
在这里插入图片描述
集合类的历史
在这里插入图片描述
在这里插入图片描述
进入查看它的方法:
在这里插入图片描述
发现了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那就加入。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值