HashMap(红黑树,线程不安全)详解

为什么HashMap是线程不安全的?(分析 jdk1.7和1.8) 前言 我们都知道HashMap是线程不安全的,只知道它源码中put/get方法都没有加同步锁,多线程操作会无法保证安全,但是除了这一点,它到底为什么线程不安全??在本章就让我们来深入了解一下HashMap线程不安全的问题。 HashMap是线程不安全的,其主要体现: 在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。 在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。 1.jdk1.7中的HashMap 在多线程环境下HashMap容易出现死循环,死循环发生在HashMap的扩容函数 阅读详情

目录

何为Hashmap

HashMap1.7put具体分析

大体流程分析

代码所涉及方法

扩容造成死循环

HashMap 1.8

红黑树

二叉查找树:中序遍历后得到一个节点递增的有序序列。

红黑树:一种含有红黑节点并且能自平衡的二叉查找树

性质

红黑树节点定义

红黑树构建及调整流程

Map 其他代码解析

keySet方法(视图方法)

tableSizeFor

hash

resize

putVal

HashMap 1.8线程不安全 


何为Hashmap

利用hash表原理进行数据存储的集合。线程不安全。

jdk1.7采用数组加链表进行存储,当遇到冲突时,利用链表存储冲突的数据。利用头插法操作链表,hashmap进行数据查询的时间复杂度取决于链表长度,时间复杂度为O(n)。在高并发的情况下会出现循环链表死循环和数据丢失。

jdk1.8采用数组加链表加红黑树进行存储,当遇到冲突时,当链表长度大于8(TREEIFY_THRESHOLD)时会把链表存储转换为红黑树进行存储。当通过删除或者分裂操作导致树中节点个数小于6时,消除树结构。

利用尾插法操作链表,hashmap进行数据查询的时间复杂度取决于红黑树,时间复杂度为O(logn)。在高并发的情况下会出现数据覆盖问题。

HashMap1.7put具体分析

大体流程分析

1.当一个函数调用put方法时,如何hashmap没有被初始化,对其进行初始化,数组初始化长度为16。计算key值得hash值,如果有值且key一样,覆盖value.如果hash值位置无值,执行addEntry。如果hash值位置已经有值,一直执行e.next,直到该hash值链表末尾,执行addEntry

2.在addEntry方法中

  2.1   插入时发现map中的键值对个数大于阈值并且在插入当前值时发生了碰撞,执行resize方法。在resize 方法中,判断数组是否等于扩容的最大值,等于时修改扩容阈值为扩容最大值。创建一个entry新数组,其长度为原数组长度的两倍。执行transfer方法。transfer方法对原数组进行循环,利用头插法把原数组中的数据插入新的数组中,最终的效果为同一位置的链表顺序翻转。返回addEntry执行table=new table,createEntr方法

 例如,假设元素组长度为2,利用3,7,5 往其中插入,在数组index=1处形成链表7->3后,把5再往其中插入,触发resize 方法,数组长度变为4,在index=3处形成链表3->7。再往新数组index=1中插入5

  2.2 插入时发现没有满足resize条件,执行createEntry方法

3.在createEntry中把数组index处的entry用一个新建的Entry  e来存放,执行Entry方法,在Entry方法中让要插入的entry的next指向e(头插法)。

代码所涉及方法

public V put(K key, V value) {
    //判断当前Hashmap(底层是Entry数组)是否存值(是否为空数组)
    if (table == EMPTY_TABLE) {
      inflateTable(threshold);//如果为空,则初始化
    }
    
    //判断key是否为空
    if (key == null)
      return putForNullKey(value);//hashmap允许key为空
    
    //计算当前key的哈希值    
    int hash = hash(key);
    //通过哈希值和当前数据长度,算出当前key值对应在数组中的存放位置
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
      Object k;
     //如果计算的哈希位置有值(及hash冲突),且key值一样,则覆盖原值value,并返回原值value
      if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
        V oldValue = e.value;
        e.value = value;
        e.recordAccess(this);
        return oldValue;
      }
    }
 
    modCount++;
    //存放值的具体方法
    addEntry(hash, key, value, i);
    return null;
  }


void addEntry(int hash, K key, V value, int bucketIndex) {
    //1、判断当前个数是否大于等于阈值
    //2、当前存放是否发生哈希碰撞
    //如果上面两个条件否发生,那么就扩容
    if ((size >= threshold) && (null != table[bucketIndex])) {
      //扩容,并且把原来数组中的元素重新放到新数组中
      resize(2 * table.length);
      hash = (null != key) ? hash(key) : 0;
      bucketIndex = indexFor(hash, table.length);
    }
 
    createEntry(hash, key, value, bucketIndex);
  }

  

void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }
   


 Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }




void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    //判断是否有超出扩容的最大值,如果达到最大值则不进行扩容操作
    if (oldCapacity == MAXIMUM_CAPACITY) {
      threshold = Integer.MAX_VALUE;
      return;
    }
 
    Entry[] newTable = new Entry[newCapacity];
    // transfer()方法把原数组中的值放到新数组中
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    //设置hashmap扩容后为新的数组引用
    table = newTable;
    //设置hashmap扩容新的阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
  }
 

transfer()在实际扩容时候把原来数组中的元素放入新的数组中

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
      while(null != e) {
        Entry<K,V> next = e.next;
        if (rehash) {
          e.hash = null == e.key ? 0 : hash(e.key);
        }
        //通过key值的hash值和新数组的大小算出在当前数组中的存放位置
        int i = indexFor(e.hash, newCapacity);
        e.next = newTable[i];
        newTable[i] = e;
        e = next;
      }
    }
  }

扩容造成死循环

https://www.cnblogs.com/developer_chan/p/10450908.html(这篇博客写的很详细)

https://blog.youkuaiyun.com/q503385724/article/details/87910929(JVM变量存储讲解)

假设数组old长度为2,数组中在index=1处存在链表7->3

现在存在两个线程同时要往old数组中插入键值对(5,null)

线程A执行插入方法,在addEntry中触发resize方法,对old数组进行循环,当e=7时,新数组index=3处为null ,即7.next=null,执行 newTable[3]=7时把该线程A挂起,此时A中java栈保存着newTable变量的引用,next=7->3,e=7,7.next=null,newTable[3]=7, 该newTable变量的引用为线程A私有

线程B执行插入方法,在addEntry中触发resize方法,对old数组进行循环。在index=3处形成链表3->7。把新数组赋值给table,新的table值存在java堆中,table值会被刷新的主存被所有线程共享,再往新数组index=1中插入5。

此时切换到线程A,执行e=next, 把e变化为3,由于B对table进行了修改,此时next=3.next,在新修改的 table中,3.next=7,所以e=7, 7.next=3,newTable[3]=7,e=3,线程A进入死循环

HashMap 1.8

红黑树

二叉查找树:中序遍历后得到一个节点递增的有序序列。

红黑树:一种含有红黑节点并且能自平衡的二叉查找树

性质

  • 性质1:每个节点要么是黑色,要么是红色。

  • 性质2:根节点是黑色。
  • 性质3:每个叶子节点(NIL或者NULL)是黑色。
  • 性质4:每个红色结点的两个子结点一定都是黑色。
  • 性质5:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。

红黑树节点定义

这个节点继承了LinkedHashMap.Entry,因为在putVal方法中回调了LinkedHashMap的方法。

而这个Entry类实际上也是继承了HashMap.Node类,所以每个TreeNode节点中含有Node.next 指针,所以每个TreeNode节点也是Node节点,所构建的链表是一个双向链表,要构建树时利用next指针得到链表中每个值

 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // 父节点
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // 指向前置节点的指针,删除Node时需要修改链接
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }

红黑树构建及调整流程

	/**
	 * tab中已经存在至少8个节点,在x为要插入的当前节点,p为从根节点开始遍历,
	 * 计算出x要插入的p的位置,如果为空,插入x,那么p就是x的父节点,x为p的子节点,如果不能插入,则继续循环,循环到可以插入,
	 * 当插入完成之后,退出当前查找插入父节点的循环
	 * 执行x=next,链表中的下一位数据进行插入 
	 */
	final void treeify(Node<K, V>[] tab) {
		TreeNode<K, V> root = null;
		for (TreeNode<K, V> x = this, next; x != null; x = next) { //利用tab的next指针对tab进行构建红黑树
			next = (TreeNode<K, V>) x.next;
			x.left = x.right = null;
			// 初始化root
			if (root == null) {
				x.parent = null;
				x.red = false;
				root = x;
			} else {
				K k = x.key;
				int h = x.hash;
				Class<?> kc = null;
				for (TreeNode<K, V> p = root;;) { //利用p查找插入节点的位置,利用hash判断,如果hash一样,利用key和 类名判断
					int dir, ph;
					K pk = p.key;
					/**
					 * 当在当前结点的右节点时 dir=-1 当在当前结点的左节点时 dir=1 当hash值相同时比较key
					 **/
					if ((ph = p.hash) > h)
						dir = -1;
					else if (ph < h)
						dir = 1;
					else if ((kc == null && (kc = comparableClassFor(k)) == null)
							|| (dir = compareComparables(kc, k, pk)) == 0)
						dir = tieBreakOrder(k, pk);// 主要是比较当key为null是时,p.k与k的hash值大小

					TreeNode<K, V> xp = p;
					if ((p = (dir <= 0) ? p.left : p.right) == null) { //p=p.right或p.left
						x.parent = xp;
						if (dir <= 0)
							xp.left = x;
						else
							xp.right = x;
						root = balanceInsertion(root, x); 
						break;  
					}
				}//插入完成,退出当前循环
			}
		}//x=next  插入链表中的下一值
		moveRootToFront(tab, root);
	}

	/**
	 * 
	 * @param root
	 * @param x  
	 *            xp x的父节点 xpp x的祖父节点
	 *            以红色节点为目标节点进入插入,每个插入的节点默认红色,以此红色节点调整父节点,并接着从祖父节点开始调整
	 *           1 当父节点为祖父节点左节点:
	 *            1.1父节点为红色时且叔叔节点为红色,直接插入
	 *            1.2父节点为红色时且叔叔节点为黑色或者不存在时:
	 *            1.2.1插入节点为父节点的右节点,左旋,xp为x左节点   让  xpp,xp,x在一条线上,再右旋 ,xpp变成x右节点
	 *           1.2.2插入节点为父节点的左节点,根据xpp进行右旋,xpp变成x右节点
	 *           2 当父节点为祖父节点右节点:
	 *           2.1 父节点为红色时且叔叔节点为红色,直接插入
	 *           2.2 父节点为红色时且叔叔节点为黑色或者不存在时:
	 *           2.2.1 插入节点为父节点的左节点,右旋,xp为x左节点   让  xpp,xp,x在一条线上,再左旋 ,xpp变成x左节点
	 *            2.2.2要插入节点为父节点的右节点,左旋,xpp变成x左节点
	 *            
	 *       当旋转执行完成之后, x等于xp,原本要插入的节点=x.parent 或者 x没有发生改变 x.parent 指向原本的祖父节点
	 *       
	 *       for (TreeNode<K, V> xp, xpp, xppl, xppr;;) 继续循环,
	 *       直到x节点变成父节点或者父节点为黑色 返回root
	 * @return
	 */
	static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root, TreeNode<K, V> x) {
		x.red = true;
		for (TreeNode<K, V> xp, xpp, xppl, xppr;;) {
			if ((xp = x.parent) == null) {  //如果x是根节点
				x.red = false;
				return x;
			} else if (!xp.red || (xpp = xp.parent) == null) // xp为黑色或者xp为根节点时返回
				return root;
			if (xp == (xppl = xpp.left)) {// 1.xp为红色,xp为父节点的左节点
				if ((xppr = xpp.right) != null && xppr.red) {// 1.1如果xppr的右节点不为空且为红色时,设置xpp的右节点为黑色,左节点为黑色,xpp为红色,调整完当前节点的上一层节点后,从xpp开始调整
					xppr.red = false;
					xp.red = false;
					xpp.red = true;
					x = xpp;
				} else { // 1.2如果xppr为黑色或者xppr不存在(叔叔节点)
					if (x == xp.right) {// 如果x节点为xp的右节点,进行一次左旋,用要插入的节点代替父节点,父节点为左节点
						root = rotateLeft(root, x = xp);// 传入根节点和 xp x.parent=pp
						xpp = (xp = x.parent) == null ? null : xp.parent; // x 变成原本父节点
																			// xpp为原本xp的父节点
					}
					if (xp != null) { // xp为新插入的节点
						xp.red = false; // 新插入xp设为黑色或者就是上个if没执行,父节点变成 黑色 
						if (xpp != null) { // 原本的祖父节点变成红色
							xpp.red = true;
							root = rotateRight(root, xpp);//传入root节点和原本的祖父节点
						}
					}
				}
			} else {//2.xp为红色,xp为xpp.right  
				if (xppl != null && xppl.red) {//2.1如果xppl不空且为红色,x的叔叔节点与父节点都为黑色,祖父节点为红色
					xppl.red = false;
					xp.red = false;
					xpp.red = true;
					x = xpp; //x变成祖父节点
				} else {  //2.2如果xppl空了或者xppl为黑色,空了及父节点叔叔节点颜色不一致说明该结构不合理
					if (x == xp.left) { //要插入节点为父节点的左节点,右旋  ,
						root = rotateRight(root, x = xp);
						xpp = (xp = x.parent) == null ? null : xp.parent;
					}
					if (xp != null) {
						xp.red = false; // 如果没有执行上个if,父节点变成黑色,执行了上个if,插入节点变成黑色
						if (xpp != null) {
							xpp.red = true; 
							root = rotateLeft(root, xpp);
						}
					}
				}
			}
		}
	}

	/**
	 * 左旋主要实现把插入的节点(本为父节点的右节点)的左节点变成父级节点,
	 * 父节点的右节点变成插入节点的左节点  执行这个方法时,x为xp的右节点,即r=x 
	 * pp 为p.parent
	 * 
	 * @param root
	 * @param p
	 * @return
	 */

	static <K, V> TreeNode<K, V> rotateLeft(TreeNode<K, V> root, TreeNode<K, V> p) {
		TreeNode<K, V> r, pp, rl;
		if (p != null && (r = p.right) != null) {// r,p都存在  p比r小
			if ((rl = p.right = r.left) != null)// 1.r的左节点变为父节点的右节点 p.right=r.left,rl=p.right,rl=r.left
				rl.parent = p;

         // r.parent=p.parent,pp=p.parent为null,当前结点设置为黑色和根节点
			if ((pp = r.parent = p.parent) == null)// 2.1
											
				(root = r).red = false;
			else if (pp.left == p) // 2.2 如果p(要插入节点r的父节点)节点为父节点的左节点
				pp.left = r;
			else
				pp.right = r;// 2.3 r节点变成父父节点的右节点
			r.left = p; // 要插入节点的左节点变成p,p的父节点变成r
			p.parent = r;
		}
		return root; // 返回根节点,看根节点是否发生改变
	}
	
	/**
	 * 右旋就是对插入节点(为连接到的父节点的左节点)与其父级节点进行替换,进行旋转,
	 * 使得父节点变成原本节点的右节点,父节点的左节点变成插入节点的右节点
	 * @param root
	 * @param p 新插入节点的父节点   l为新插入节点的原本父节点
	 * @return
	 */
    
	static <K, V> TreeNode<K, V> rotateRight(TreeNode<K, V> root, TreeNode<K, V> p) {
		TreeNode<K, V> l, pp, lr;
		if (p != null && (l = p.left) != null) {//p,l 存在    p比l大
			if ((lr = p.left = l.right) != null) //l的右节点变为父节点的左节点
				lr.parent = p;
			if ((pp = l.parent = p.parent) == null)//如果祖父节点p不为根节点,l取代父节点的位置
				(root = l).red = false;
			else if (pp.right == p) 
				pp.right = l;  
			else
				pp.left = l;
			l.right = p;   //l的右节点指向父节点
			p.parent = l;
		}
		return root;
	}

https://www.cnblogs.com/oldbai/p/9888370.html  带图解释左旋代码,还比较清晰

Map 其他代码解析

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

keySet方法(视图方法)

HashMap 继承了一个AbstractMap类,实现了Map接口。在AbstractMap类中最重要的是keySet方法。keySet是一个视图方法,在keySet中并没有实际的查询map中的key保存到一个set中,而是利用重写的Iterator方法,当要获得keySet 然后对每个key值使用Iterator时,才对map中的table进行查询。

tableSizeFor

通过位移把cap的低位全变为1,最后获得最接近比cap大的2的次幂数组容量。例如cap=9,其补码为0101最后获得的n的补码为0111,n+1最后返回的capcity补码即为1000

 static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

hash

key.hashcode左移16位异或key.hashcode,这样做相较1.7较好保存了高位信息,这么做可以在数组table的length比较小的时候,也能保证高低位参与到hash计算中,又不会有很大的消耗


 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

resize

初始化:在resize 方法中会根据两种构造函数分别创建不同长度的table

1.使用默认构造函数,会进入else条件,创建一个数组长度为16,阈值为12的新数组

2.使用带容量的构造函数,进入else if ,创建一个数组长度为 tableSizeFor(initialCapacity) ,

  阈值为tableSizeFor(initialCapacity)*loadFactor的新数组

如果是扩容而不是初始化:数组长度变为原来两倍,阈值也变为原来两倍

https://blog.youkuaiyun.com/soga613/article/details/78958642  讲resize 讲的很不错

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table; //首次初始化后table为null
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;  //使用默认构造器的threshold为0
        int newCap, newThr = 0;
        if (oldCap > 0) {  //3.执行过 resize
            if (oldCap >= MAXIMUM_CAPACITY) { //已经达到最大值,无法进行扩容 
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)  // threshold 与 table.length都将变成两倍
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // 2.initial capacity was placed in threshold  
        //使用带initialCapacity的构造器threshold=tableSizeFor(initialCapacity) 
            newCap = oldThr;
        else {               // 1.zero initial threshold signifies using defaults //使用默认无参数构造函数
            newCap = DEFAULT_INITIAL_CAPACITY; //默认16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //默认计算出为12
        }
        if (newThr == 0) {  //使用带initialCapacity的构造器进入这个if 把newThr变成threshold*loadFactor
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //根据获得的新的table长创建一个新的table,实现初始化或者扩容
        table = newTab;
        if (oldTab != null) {  //tabel!=null 把oldtab中的数据放入newtab
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)  // 如果e为oldTab[j]链表处的唯一值
                        newTab[e.hash & (newCap - 1)] = e; //把e插入新表
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do { //在oldTab[j]处发生了hash冲突
                            next = e.next;
                            if ((e.hash & oldCap) == 0) { //因为新的数组长度为旧数据长度的两倍,执行n-1之后,新的数组长度的二进制表示就比旧的数组长度的二进制表示多个高位1,如果利用e.hash值与新的数组长度进行与操作,如果最高位为0,那么e所在位置与未扩容之前一样
                            通过e.hash & oldCap计算出新增bit位到底为0还是为1 为0时表示e在扩容后位置没有发生改变
                                if (loTail == null)      //loTail 中存储不需要移动的节点
                                    loHead = e;          //loHead 中存储冲突链表第一个不需要移动的节点,便于进行放置
                                else
                                    loTail.next = e;     //尾插法,链表顺序不发生改变
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)   //hiTail 中存储需要移动的节点
                                    hiHead = e;       //hiHead 中存储冲突链表第一个需要移动的节点,便于进行放置
                                else
                                    hiTail.next = e;  //尾插法,链表顺序不发生改变
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;  //j+oldCap为新的链表中的位置
                        }
                    }
                }
            }
        }
        return newTab;   //返回扩容完的tab
    }

putVal

在冲突处的链表长度大于等于9时执行treeifyBin,在该方法中只有当table的长度(非entrySet个数,是数组长度)大于64,才会把Node链表转化为TreeNode链表,然后开始构建红黑树。如果table长度小于64 只会进行扩容来消除hash冲突

 /**
     * 
     * @param hash hash(key)
     * @param key
     * @param value
     * @param onlyIfAbsent false
     * @param evict true
     * @return
     */
	final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
		Node<K, V>[] tab;
		Node<K, V> p;
		int n, i;
		if ((tab = table) == null || (n = tab.length) == 0)  //初始化
			n = (tab = resize()).length; 
		if ((p = tab[i = (n - 1) & hash]) == null) //计算出 p在table中的位置,如果为空,直接插入
			tab[i] = newNode(hash, key, value, null);
		else {//在table[i]处已经有值
			Node<K, V> e;
			K k;
			//如果要插入的数和p相同
			if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
				e = p;
			//table[i]处是一个红黑树
			else if (p instanceof TreeNode)
				e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
			else {//table[i]处是一个 链表
				for (int binCount = 0;; ++binCount) {
					if ((e = p.next) == null) { //把要插入的数据插入链表再判断链表中是否已经有8个节点.所以当实际进行treeifBin时链表中有9个结点
						p.next = newNode(hash, key, value, null);
						if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
							treeifyBin(tab, hash); //判断数组长度是否小于64,如果是,扩容,如果不是,把Node链表变成TreeNode链表
						break;
					}
					if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))//如果e与要插入的节点相同,退出循环
						break;
					p = e;
				}
			}
			if (e != null) { // existing mapping for key  
				V oldValue = e.value;
				if (!onlyIfAbsent || oldValue == null) //onlyIfAbsent 传值false 
					e.value = value;  //覆盖value
				afterNodeAccess(e);  //调用LinkedHashMap中的afterNodeAccess,实现将操作过的节点放在最后一个
				return oldValue;
			}
		}//e 为空才能执行这下面的操作 就是当执行第一个if发现table[i]出存在的节点为null,且要插入的节点也为null
		++modCount;
		if (++size > threshold) 
			resize();
		afterNodeInsertion(evict); //删除最老的头节点
		return null;
	}
	
	
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //如果当前链表的长度小于64,扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize(); 
        else if ((e = tab[index = (n - 1) & hash]) != null) {//如果数组长度大于64
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null); //创建新的 TreeNode节点
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null); //把Node链表转换为TreeNode链表
            if ((tab[index] = hd) != null) //构建红黑树
                hd.treeify(tab);
        }
    }

HashMap 1.8线程不安全 

在HashMap1.8中采用尾插法插入时不会发生环形链表了,但是如果两个线程同时执行一个if ((p = tab[i = (n - 1) & hash]) == null) ;tab[i] = newNode(hash, key, value, null);  是可能发生数据丢失的。

参考链接

https://www.cnblogs.com/developer_chan/p/10450908.html(详解hashmap线程不安全 )

https://blog.youkuaiyun.com/insomsia/article/details/88367436(jdk1.7头插法解析)

https://blog.youkuaiyun.com/chen13333336677/article/details/99706702(jdk1.7扩容机制详解)

 

一文带你速通HashMap底层核心数据结构红黑树 每个节点或者红色,或者是黑色。根节点为黑色。每一个叶子节点都是黑色。如果一个节点是红色,那么他的孩子节点都是黑色。从任意一个节点,经过的黑色节点是一样的。在《算法4》一书中认为红黑树和2-3树是等价的。 阅读详情

相关推荐

本地部署 pyvideotrans(视频翻译配音工具)

语音识别选择faster-whisper(本地),模型选择预先下载并保存的medium,然后选择好要处理的视频以及保存到的位置,这里我选择了一个名为why not的英文演讲视频,保存在桌面_vedio_out文件下。将附件中faster-medium.7z 和 faster-small.7z解压到项目中的models目录下, 解压后,目录中应该存在 models--Systran--faster-whisper-small。en.m4a|.wav = 原始视频中的音频文件(包含所有背景音和人声)

西唯兵欧泡的博客 2342

HashMap原理浅析(关于红黑树是什么?)

HashMap 1. HashMap的数据结构 数据结构中有数组和链表来实现对数据的存储,但这两者基本上是两个极端。 数组 数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难; 链表 链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。 哈希表 那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希

weixin_45105261的博客 4902

动态博弈、威胁与承诺(博弈论入门学习笔记四)

0 动态博弈 动态博弈:行动有先后顺序,不同的参与人在不同时点行动,先行动者的选择影响后行动者的选择空间,后行动者可以观察到先行动者做了什么选择。例如下棋、消费者和商家讨价还价、谈婚论嫁、企业之间的价格战等等。 因此,为了做出最优的行动选择,每个参与人都应该思考:如果我如此选择,对方将如何应对?如果我是他,我将会如何行动?给定他的应对,什么是我的最优选择? 动态博弈中的战略:在博弈开始之前就...

stepho的博客 1万+

HashMap-红黑树

先简单了解下红黑树 红黑树本质上是一种二叉查找树,但它在二叉查找树的基础上额外添加了一个标记(颜色),同时具有一定的规则。这些规则使红黑树保证了一种平衡,插入、删除、查找的最坏时间复杂度都为 O(logn)。 红黑树的特性: 每个节点要么是红色,要么是黑色; 根节点永远是黑色的; 所有的叶节点都是是黑色的(注意这里说叶子节点其实是上图中的 NIL 节点); 每个红色节点的两个子节点一定都是黑色; 从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点; 左旋右旋: 红黑树的左右旋是比

L.的博客 1587

从零构建AI智能体:阿里云百炼平台实战指南(一)

本文是阿里云百炼平台构建AI智能体的实战指南。文章详细阐述了从聊天机器人到具备自主规划与执行能力的AI智能体的概念演进,并手把手指导读者在百炼平台上快速创建专属智能体、编排复杂工作流,以及实现多智能体协同,为开发者提供一站式、低门槛的大模型应用开发解决方案。

ol7890123的博客 296

宜搭,高级认证实操2,FaaS连接器指导教程

找到FaasEntry.java将如下代码整体替换,注意引包。如图所示在util下新建EDSUtil.java代码如下。回到连接器界面点击右上角测试-->测试。连接器工厂-->新建FaaS连接器。保存-->前往云IDE开发。返回如图所示代码,部署成功。

2301_80286061的博客 3343

HashMap底层红黑树原理(超详细图解)+手写红黑树代码

在看完了小刘老师和黑马的源码视频之后,我整理了一篇HashMap的底层源码文章,学海无涯,这几天看了对红黑树的讲解,故将其整理出来 HashMap底层源码解析上 HashMap底层源码解析下 视频链接 小刘老师讲源码 传智播客-黑马程序员 文章目录红黑树什么是树 红黑树 什么是树 树(tree)是一种抽象数据类型(ADT),用来模拟具有树状结构性质的数据集合,它是由n(n>0)个有限节点通过连接它们的边组成一个具有层次关系的集合。把它叫做“树”是因为他看起来像一颗倒挂的树,也就是说它是根朝上,而叶向

wenwenaier的博客 2万+

SAR ADC和sigma delta ADC的区别

0、ADC基本概念 ADC,即analog to digital converter,模拟到数字的转换器,简称模数转换器。 为什么需要ADC?因为现实的世界中,电信号是连续的,例如心电信号,或者声音信号,这些连续的信号被称为“模拟信号”。 图1:心电信号 图2:声音信号 然而这种连续的信号是无法被计算机处理的,计算机只认识0和1组成的“数字信号“。ADC就充当了这样一个桥梁,将“模拟信号”转换为计算机能识别的“数字信号”。举个例子,当前的心电电压为1.2V(实际没有这么高),ADC可能会得

长河柱天的博客 2万+

四大集合之HashMap详解

此时如果有两个线程同时进入if语句,线程A把数据插入到表头,接着线程B把他的数据覆盖到表头,这样就产生了数据覆盖的问题,线程A的数据相当于消失了。更小,且JDK8版本的链表还可以升级为查询效率高的红黑树,所以JDK7版本的ConcurrentHashMap目前被JDK8版本的代替了。Node节点的内部属性key、value分别代表键和值,hash代表key的hash值,而next则是指向下一个链表节点的指针。但实际上期望的size应该是2,此时就出现了数据不一致的问题,Entry数组的容量会出现错误。

程序员-杨胡广的博客 977

畅游Diffusion数字人(0):专栏文章导航

畅游Diffusion数字人(0):专栏文章导航

沉迷单车的追风少年 1421

059、肤色还原专项调优:记忆色、颜色偏好建模与主观评价标准

文章摘要:肤色还原调优的技术挑战与实践经验 本文系统探讨了手机影像中肤色还原的技术难点与解决方案。通过实际案例揭示了肤色还原的本质是记忆色与颜色偏好的博弈,而非单纯的技术指标达标。文章详细分析了记忆色的三大特性(高饱和、地域差异、场景依赖)和颜色偏好的数学建模方法,提出了基于YCbCr/Lab空间的参数化模型和代价函数设计。在评价体系方面,强调主观评价的重要性,并分享了专家评审、用户调研、场景覆盖的三级验证方法。最后总结了实战中的关键经验,包括肤色检测优化、过渡处理、动态范围管理及视频/拍照差异策略。文章指

sundong_81的博客 250

HashMap底层原理2

两个线程,假设HashMap的size为15,线程A从主内存获得15,准备进行++的操作的时候,被挂起,然后线程B拿到size并执行++操作,并写回主内存,这时size是16,然后线程A继续执行(这时A线程内存size还是15)++操作,然后写回主内存,即线程A和线程B都进行了put操作,然后size值增加了1,所以数据被覆盖了。继续执行线程A,执行 newTable[i] = e,因为当时 e = 3,所以将3放到对应位置,此时执行 e = next,因为 next = 7(第②步),所以 e = 7。

qq_63126439的博客 1549

华为流程L1-L6业务流程深度细化到可执行【附全文阅读】

该文档主要介绍了华为业务流程的深度细化及相关内容,包括流程框架、建模方法、流程模块描述、流程图建模等,旨在帮助企业构建有效的流程体系,实现战略目标。

cdfunlove的博客 4003

最详细HashMap底层详解(附常见面试题)

Map家族 先来看看HashMap在Map这个大家族中的位置。 这四种比较常见:HashMap , Hashtable , LinkedHashMap,TreeMap HashMap存取速度快,它根据键的hashcode值存储数据,线程不安全,只有一个键允许为null,对值没有限制,对数据进行遍历的话读取的随机的。如果想要同步,可以用Collections的synchronizedMap方法使H...

愿万事胜意 7586

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

Set ,HashSet ,哈希表(数组+链表+红黑树),TreeSet ,Map,HashMap,TreeMap,Properties ,Collections, 如何处理HashMap的线程不安全

Set : 无序的 不可重复 * 新增功能: static <E> Set<E> of(E... elements) 返回包含任意数量元素的不可修改集。 * 其他方法从Collection继承 * 遍历方式:* foreach* iterator * * 注意: * 存放数据的顺序与内部真实存储的顺序可能不一致 public class Class001_Set { public static void main(S...

wangjinju77777的博客 295

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

集合框架Map——HashMap

dd

weixin_39568038的博客 181

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

HashMap真的是大于8就转换成红黑树,小于6就变成链表吗???

这篇文章仅限小编个人的理解,小编不是Java方向的,只是对Java有很高的学习兴趣 如果有什么不对的地方还望大佬指点 QQ交流群: 99979568 HashMap的底层是数组+链表,(很多人应该都知道了) JDK1.7的是数组+链表 (1.7只是一个例子,以前的话也是这样后面就以1.7为例子了) 首先是一个数组,然后数组的类型是链表 元素是头插法 JDK1.8的是数组+链表 或者 数组+红黑树 首先是一个数组,然后数组的类型是链表 在链表的元素大于8的时候,会变成红黑树 在红黑树的元素小于6的时候会变.

日常发呆 2万+

HashMap 核心原理

选择标准单线程场景:优先使用 HashMap。多线程场景:使用 ConcurrentHashMap,避免 HashMap。性能优化预估初始容量(如),减少扩容次数。自定义 key 类时重写hashCode()和equals()方法,降低哈希冲突。注意事项避免在多线程环境下对 HashMap 进行 put 操作,否则需外部同步(如理解红黑树与链表的转换条件,避免频繁树化 / 退化影响性能。

sc35262的博客 694

hashmap为什么是线程不安全的_为什么HashMap线程不安全

一、Map概述我们都知道HashMap是线程不安全的,但是HashMap的使用频率在所有map中确实属于比较高的。因为它可以满足我们大多数的场景了。Map类继承图上面展示了java中Map的继承图,Map是一个接口,我们常用的实现类有HashMap、LinkedHashMap、TreeMap,HashTable。HashMap根据key的hashCode值来保存value,需要注意的是,HashM...

weixin_39862899的博客 158

HashMap 面试全攻略

HashMap 本质是空间换时间JDK 1.8 的核心升级是:红黑树尾插法高效 resize面试重点永远在:put / getresize并发问题HashMap 的精髓不是 API,而是在冲突、扩容、并发之间做工程权衡。

zhiyuanIT的专栏 839

hashmap的底层原理_面试倒在了HashMap上?别急HashMap面试题,看这一篇就够了

欢迎关注专栏《Java架构筑基》——专注于Java技术的研究与分享!Java架构筑基Java架构筑基——专注于Java技术的研究与分享!后续文章将首发此专栏!欢迎各位Java工程师朋友投稿和关注序言在后端的日常开发工作中,集合是使用频率相当高的一个工具,而其中的HashMap,则更是我们用以处理业务逻辑的好帮手,同时HashMap的底层实现和原理,也成了面试题中的常客。以前曾有详细了解过HashM...

weixin_39888049的博客 115

hashmap详解

线程A在操作数据的时候,线程B只能阻塞。4. 一个事务读取了另一个事务未提交的数据(常常是并发操作引起的,并发操作所带来了一致问题包括:丢失的修改,不可重复的读,读“脏”数据,幻读),当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问,直到该线程读取完,其他线程才可以使用。c. StringBuilder是非线程安全的,StringBuffer是线程安全的。b. HashMap是非线程安全的,HashTable是线程安全的;a. ArrayList是非线程安全的,Vector是线程安全的。

weixin_44838773的博客 138

hashmap的容量是链表加数组吗_HashMap结构组成及扩容原理

在之前对HashMap有一个自己的总结文章,近期重新温习了一遍HashMap,发现又学到了很多新的知识点,并且原来的很多观点都是不太正确的;加耀:浅谈HashMap​zhuanlan.zhihu.com以下是对hashMap新的总结: HashMap的数据结构 Map是一种很重要的数据结构,在日常开发中几乎是无处不在用;在数据结构章节我们都知道数组查询效率高,但是增删效率低,而链表增删效率高,而查...

weixin_30310149的博客 692

hashmap为什么是线程不安全的_唯品会面试题:为什么HashMap线程不安全

一、Map概述我们都知道HashMap是线程不安全的,但是HashMap的使用频率在所有map中确实属于比较高的。因为它可以满足我们大多数的场景了。Map类继承图上面展示了java中Map的继承图,Map是一个接口,我们常用的实现类有HashMap、LinkedHashMap、TreeMap,HashTable。HashMap根据key的hashCode值来保存value,需要注意的是,HashM...

weixin_39917791的博客 93

【Java基础】HashMap、TreeMap、都用了红黑树

引子 昨天模拟面试,面试官问到了 哈希map 和 treeMap 我说都是使用了 红黑树 问我有什么区别 还有复杂度 稍微一深入讨论 我就废掉了 先亡羊补牢一下 文章目录引子1)、使用层次上的区别:HashMap:TreeMap:2)、底层数据结构HashMap:HashTree:总结:红黑树特征:红黑树左旋、右旋: 1)、使用层次上的区别: HashMap: 数组+链表存储key-value,1.8加入红黑树(优化链表查找过长的问题) 允许null作为key和value,key不可以重复,value允许

韩旭051的博客 915

面试官爆锤HashMap:HashMap实现原理?HashMap是线程安全的吗?

面试题1:说一下 HashMap 的实现原理? 追问1:如何实现HashMap的有序? 追问2:那TreeMap怎么实现有序的? 追问3:put方法原理是怎么实现的? 追问4:HashMap扩容机制原理 追问5:HashMap在JDK1.8都做了哪些优化? 追问6:链表红黑树如何互相转换?阈值多少? 面试题2:HashMap是线程安全的吗? 追问1:你是如何解决这个线程不安全问题的? 追问2:说一下大家为什么要选择ConcurrentHashMap? 追问3:ConcurrentHashM

Trouvailless的博客 294
上一篇: hadoop 之hdfs知识详细整理
下一篇: hadoop windows安装
date-date
博客等级 码龄9年 6粉丝 28原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值