2021年JAVA面试~初识集合Map(二)

前言

在这里插入图片描述
各位靓仔好呀,我们接着讲讲Map集合,
我们知道HashMap是线程不安全的及不安全在哪里
今天主要介绍线程安全的Map

上一篇文章地址:https://blog.youkuaiyun.com/u013351145/article/details/117450011

当然靓仔们可以看看我整理的系列:光头佳的求职之旅

https://blog.youkuaiyun.com/u013351145/category_11030408.html

在并发编程的现在,为了保持数据安全,HashMap线程不安全的问题注定不适合多线程并发。
因此也是在集合方面面试询问的高发区

HashTable

Hashtable是Map接口下的实现类之一,属于线程安全的集合。

下图为继承关系图
在这里插入图片描述
Hashtable是继承Dictionary类与HashMap继承AbstractMap不同,

初始化

Hashtable是默认长度11,加载因子0,75,支持数据结构数组和链表。

	// 存放数组
    private transient Entry<?,?>[] table;
	// 当前行数
    private transient int count;
	// 阈值
    private int threshold;
	// 加载因子
    private float loadFactor;

    private transient int modCount = 0;
	
	// 用指定的初始容量和负载因子构造一个新的空哈希表
    public Hashtable(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);

        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry<?,?>[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }
	
	// 指定长度和默认加载因子0.75
    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }
	
	// 默认11
    public Hashtable() {
        this(11, 0.75f);
    }

新增元素

	// key与value不允许为空
    public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        // 判断下标位
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
	
        addEntry(hash, key, value, index);
        return null;
    }

	// 新增元素
    private void addEntry(int hash, K key, V value, int index) {
        modCount++;

        Entry<?,?> tab[] = table;
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            // 扩容
            rehash();

            tab = table;
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }
	// threshold初始阈值是MAX_ARRAY_SIZE + 1,扩容大小原有的2倍+1
    protected void rehash() {
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;

        // overflow-conscious code
        int newCapacity = (oldCapacity << 1) + 1;
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

        modCount++;
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        table = newMap;

        for (int i = oldCapacity ; i-- > 0 ;) {
            for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
                Entry<K,V> e = old;
                old = old.next;

                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = (Entry<K,V>)newMap[index];
                newMap[index] = e;
            }
        }
    }

新增元素问题

1.为啥说HashTable线程安全呢?
因为所有操作方法上有synchronized 关键字来保证线程安全

2.synchronized 是什么东东,凭这个关键字就可以保证?
简单来说synchronized 可以看成一个锁,A,B同时去添加新元素,
A先加synchronized锁,
这样B去添加的时候因为加了锁,无法添加
需要A添加完锁之后把锁打开,B去添加的时候也会加把锁,
假设又来了一个C就会陷入B之前的难题。

在这里插入图片描述

换成滑滑梯理解的话,熊猫A被熊猫B的萌物冲击顶到了大腰子,
下次再滑的时候在入口加了栏杆,只有自己滑到底去把栏杆拿掉,
熊猫B才能爬上滑滑梯,这样就很好的保护了自己的大腰子
在这里插入图片描述

挖坑
synchronized 作为并发编程锁之一,我准备在多线程的时候再讲,
先挖个小坑,以为我风一般的更新速度马上就到了。

3.HashTable这么好为啥不用
因为安全,成也萧何败也萧何,
安全的提前是牺牲了熊猫B的滑滑梯时间,
那没有更好的方法吗,接下来介绍下一位主角。

ConcurrentHashMap

ConcurrentHashMap是线程安全的map集合,初始化HashMap基本一致,我就不过多介绍,
直接说结论ConcurrentHashMap线程安全的实现,1.7和1.8方式不同,
1.8:CAS锁(算法)+synchronized
1.7:利用Segment,实现可重入式分段锁

1.8 新增元素put()

    public V put(K key, V value) {
        return putVal(key, value, false);
    }
	
    final V putVal(K key, V value, boolean onlyIfAbsent) {
    	// 看来key和value不允许为空
        if (key == null || value == null) throw new NullPointerException();
        // 计算hash值
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            // 初始化数组和判断下标位与hashMap一致的
            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
            }
            // 扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
            	// 最后的方式添加synchronized 然后走HashMap树化扩容treeifyBin
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    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;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

    static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }

    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }
	
	// 扩容
    final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
        Node<K,V>[] nextTab; int sc;
        // 检验判断非空
        if (tab != null && (f instanceof ForwardingNode) &&
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
            // 根据resizeStamp拿到一个标识
            int rs = resizeStamp(tab.length);
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    transfer(tab, nextTab);
                    break;
                }
            }
            return nextTab;
        }
        return table;
    }

1.比HashTable多了一个CAS,什么是CAS?
CAS算法:(Compare And Swap)比较交换,而synchronized不管三七二十一先把锁加上,
CAS算法中包含三个参数(V,E,N),V表示要更新的变量、E表示预期的值、N表示新值。
我现在要改一个值,我知道原来要更新的V=1,那么E等于我所有知道的V=1
,现在我N=V+1把三个值去覆盖更新,重新获取一次V,发现V已经是2,那么说明有人抢在我前面更新了。
其实V和N很好理解的,E可能会混乱你的思路。

有一次A熊猫忘记了把栏杆拿下来,B熊猫被栏杆卡主无法爬上滑滑梯,哭着去找园长,园长就是把滑滑梯改造了一下。
滑滑梯上会显示号码,你去园长那边领一张票,如果票上的号码和滑滑梯的号码一样,你就可以去爬上去滑。
如果不对再去找园长那一张新的票,票就是E,号码对了才能使用。

1.7 新增元素put()

通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。

锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

		// Segment 数组,存放数据时首先需要定位到具体的 Segment 中。
		final Segment<K,V>[] segments;
		// 
	    public V put(K key, V value) {
	        Segment<K,V> s;
	        if (value == null)
	            throw new NullPointerException();
	        int hash = hash(key);
	        int j = (hash >>> segmentShift) & segmentMask;
	        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
	            (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
	            s = ensureSegment(j);
	       return s.put(key, hash, value, false);
	   }

1.为啥1.8里放弃使用Segment 数组?
还是我们老生常谈的问题,时间与空间,分成16端是很帅,变相的将写入操作加快16倍,但是显而易见的空间大大的被浪费。
第二个关键点:现实生活中 map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。

小结

关于HashTable和ConcurrentHashMap,相比较HashMap我没有过多的介绍,
目前维持多线程安全问题解决方式就是锁,各种各样的锁,
从公平性出发:公平锁,非公平锁
从应用出发:轻量级,重量级,
及自旋锁,排它锁,可重入锁…

太多了,我更想在并发编程的时候介绍清楚。

我咋感觉你就是想偷懒?
在这里插入图片描述

TreeMap

我们知道HashMap是无序的,TreeMap与HashMap相比,TreeMap是一个能比较元素大小的Map集合,会对传入的key进行了大小排序。

看这个英文就知道TreeMap是红黑树结构

下图为继承关系
在这里插入图片描述

初始化

	// 比较器
    private final Comparator<? super K> comparator;
    
    private transient Entry<K,V> root;

    private transient int size = 0;

    private transient int modCount = 0;
	
	// 构造默认无比较器的treeMap
    public TreeMap() {
        comparator = null;
    }
	// 构造指定比较器的treeMap
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }

新增元素

    public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        // 如果比较器不为空
        if (cpr != null) {
            do {
            	// 从根节点开始
                parent = t;
                // 比较当前key和出入的key的大小
                cmp = cpr.compare(key, t.key);
                // 小的话,往左边移动
                if (cmp < 0)
                    t = t.left;
                // 大的话,往右边边移动
                else if (cmp > 0)
                    t = t.right;
                else
                	// 如果相等就覆盖,更新
                    return t.setValue(value);
            } while (t != null);
        }
        // 在没有比较器的情况下,调用自然排序的Comparable比较
        else {
        	// key不能空
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

小结

开发过程中较少使用到这个TreeMap因此也没啥较深的理解

在这里插入图片描述

到这里集合介绍的差不多,应该还会出一些面试题,
其中有别的面试官问我的,也有我觉得该掌握的点。

最后

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值