并发基础_10_并发_容器_ConcurrentHashMap

ConcurrentHashMap


HashMap多线程死循环Demo

在之前HashMap的文章中提到,HashMap是非线程安全的。

在多线程环境下使用HashMap的put操作会引发死循环,所以在并发/多线程情况下不能使用HashMap。

我们看下面的这个Demo

package com.hashmap.hashmap_1;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

public class HashMapThread extends Thread {

	private static AtomicInteger ai = new AtomicInteger(0);// 以原子方式增加的计数器

	private static Map<Integer, Integer> map = new HashMap<Integer, Integer>();

	@Override
	public void run() {
		while (ai.get() < 10000000) {
			map.put(ai.get(), ai.get());
			ai.incrementAndGet();// 原子方式int值+1
		}
	}

}

package com.hashmap.hashmap_1;

public class TestMain {

	public static void main(String[] args) {

	HashMapThread t0 = new HashMapThread();
	HashMapThread t1 = new HashMapThread();
	HashMapThread t2 = new HashMapThread();
	HashMapThread t3 = new HashMapThread();
	HashMapThread t4 = new HashMapThread();
	HashMapThread t5 = new HashMapThread();
	HashMapThread t6 = new HashMapThread();
	HashMapThread t7 = new HashMapThread();

	t0.start();
	t1.start();
	t2.start();
	t3.start();
	t4.start();
	t5.start();
	t6.start();
	t7.start();

	}
}


输出结果:

程序一直在死循环,很长时间了..

这个要多执行几次,有几次是下标越界,还有遇到过内存溢出的情况,多执行几次,就能出现死循环..

使用jstack查看线程状态


发现是HashMap在put操作、扩容时,引发死循环。
因为多线程会导致HashMap的Entry链表形成了环形数据结构,从而引发死循环。


ConcurrentHashMap的锁分段技术在并发场景下,为什么效率好?

原书的描述:

HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁。

ConcurrentHashMap首先将数据分成一段段的存储,然后给每一段数据配上一把锁,当一个线程占用锁访问其中

一段数据时,其他段的数据也能被其他线程访问



HashTable和ConcurrentHashMap的对比

HashTable为什么是线程安全的?

我们看下HashTable对外方法的源码:

public synchronized int size() {
	return count;
}

public synchronized boolean isEmpty() {
	return count == 0;
}

public synchronized Enumeration<K> keys() {
	return this.<K>getEnumeration(KEYS);
}

public synchronized V get(Object key) {
	Entry tab[] = table;

	// 计算key的hash值
	int hash = key.hashCode();
	// 计算hash值在table数组中的下标
	int index = (hash & 0x7FFFFFFF) % tab.length;

	// 从链表Entry中取出value值
	for (Entry<K, V> e = tab[index]; e != null; e = e.next) {
			if ((e.hash == hash) && e.key.equals(key)) {
					return e.value;
			}
	}
	return null;
}

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

	/**
	 * 处理key在table数组中是不重复的<br>
	 * 处理过程:<br>
	 * 1.计算key的hash值,确认在table数组中的索引位置<br>
	 * 2.迭代index索引位置,如果该位置处的链表中存在一个一样的key,则替换其value,返回旧值<br>
	 */
	// Makes sure the key is not already in the hashtable.
	Entry tab[] = table;
	int hash = key.hashCode();// 计算key的hash值
	int index = (hash & 0x7FFFFFFF) % tab.length;// 确认该key的索引位置

	// 迭代,寻找该key,替换
	for (Entry<K, V> e = tab[index]; e != null; e = e.next) {
			if ((e.hash == hash) && e.key.equals(key)) {
					V old = e.value;
					e.value = value;
					return old;
			}
	}

	modCount++;
	// 如果容器中的元素数量达到阀值,则进行扩容操作
	if (count >= threshold) {
			// Rehash the table if the threshold is exceeded
			rehash();

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

	// Creates the new entry.
	// 在索引位置处插入一个新的节点
	Entry<K, V> e = tab[index];
	tab[index] = new Entry<>(hash, key, value, e);
	// 容器中元素+1
	count++;
	return null;
}

public synchronized void putAll(Map<? extends K, ? extends V> t) {
	for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
			put(e.getKey(), e.getValue());
}

.......

通过上面HashTable对外提供的方法(public方法)源码,发现什么没有??

所有方法的源码都加上了synchronized锁!


也就是说,在操作HashTable对象容器时,它给整个容器加了一个锁。

当线程1获取锁,操作HashTable对象容器时,其他的线程只能等着,等线程1干完了,才能获取锁。


看过HashMap源码的童鞋肯定知道,HashMap的底层实现是数组+链表;

ConcurrentHashMap将数组分段,分段存储数据,给每个分段加上锁;

当线程1进入ConcurrentHashMap对象容器中操作数据时,先找到对应的分段数据,然后将此分段数据加锁,接着操作数据。

其他线程进入ConcurrentHashMap对象容器中操作数据时,只要不是和线程1一样的分段数据,都不会有影响。


说简单些,ConcurrentHashMap就是降低锁的颗粒度。

(网上找的图片,感谢原作者)

ConcurrentHashMap内部存储结构

(感谢原作者图片)

简单说下:

Segments:就是整个Hash表

Segment:段(分段数据)

table:一个table就相当于一个HashTable。

(其实我觉得原博这样说会有歧义,HahsMap底层是由数组+链表组成,我觉得这个table就是数组中几个相连元素组成的一个数据段,

就比如HashMap的table[]数组的长度是16,ConcurrentHashMap的table中就是HahsMap中的table[0]+table[1]+table[2]+table[3]组成的一个数据段)


HashEntry

HashEntry是最底层存储元素的,HashEntry自己维护了链表

ConcurrentHashMap中的HashEntry比HashMap中的Entry要简洁

参数定义和HashMap中的Entry一样。

static final class HashEntry<K, V> {
	final int hash;
	final K key;
	volatile V value;
	volatile HashEntry<K, V> next;

	HashEntry(int hash, K key, V value, HashEntry<K, V> next) {
			this.hash = hash;
			this.key = key;
			this.value = value;
			this.next = next;
	}

	/**
	 * Sets next field with volatile write semantics. (See above about use
	 * of putOrderedObject.)
	 */
	final void setNext(HashEntry<K, V> n) {
			UNSAFE.putOrderedObject(this, nextOffset, n);
	}

	// Unsafe mechanics
	static final sun.misc.Unsafe UNSAFE;
	static final long nextOffset;
	static {
			try {
					UNSAFE = sun.misc.Unsafe.getUnsafe();
					Class k = HashEntry.class;
					nextOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("next"));
			} catch (Exception e) {
					throw new Error(e);
			}
	}
}


Segment段

Segment继承了ReentrantLock(重入锁),使得Segment可以充当锁的角色。

每个Segment对象守护其中包含的多个桶(多个Entry链表)。

我们看下Segment的源码:

static final class Segment<K, V> extends ReentrantLock implements Serializable {

	static final int MAX_SCAN_RETRIES = Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
	
	//table是HashEntry对象组成的数组
	//table数组的成员代表一个HashEntry链表
	//每个table守护整个ConcurrentHashMap中的一部分
	//如果concurrencyLevel为16,那么table则守护ConcurrentHashMap总数据的1/16
	transient volatile HashEntry<K, V>[] table;
	
	//在本segment范围内,包含HashEntry元素的个数
	transient int count;

	//table被更新的次数
	transient int modCount;

	transient int threshold;

	final float loadFactor;

	Segment(float lf, int threshold, HashEntry<K, V>[] tab) {
			this.loadFactor = lf;
			this.threshold = threshold;
			this.table = tab;
	}

}

count:该变量是一个计数器,它表示每个Segment对象管理的table数组(若干个HashEntry组成的链表)包含HashEntry对象的个数。

每一个Segment对象内部都有一个count属性来代表自己Segment中包含的HashEntry对象的总数。


之所以在每个Segment对象中包含一个计数器,而不是在ConcurrentHashMap中使用全局的计数器,是为了避免出现"热点域"而影响

ConcurrentHashMap的并发性(来自原博,个人保留意见)



ConcurrentHashMap构造方法

ConcurrentHashMap():

创建一个带有默认初始容量 (16)、加载因子 (0.75) 和 concurrencyLevel (16) 的新的空映射。


ConcurrentHashMap(int initialCapacity):

创建一个带有指定初始容量、默认加载因子 (0.75) 和 concurrencyLevel (16) 的新的空映射。


ConcurrentHashMap(int initialCapacity, float loadFactor):

创建一个带有指定初始容量、加载因子和默认 concurrencyLevel (16) 的新的空映射。


ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)

创建一个带有指定初始容量、加载因子和并发级别的新的空映射。 


ConcurrentHashMap(Map<? extends K, ? extends V> m)

构造一个与给定映射具有相同映射关系的新映射。参数默认


关于concurrencyLevel参数,源码中的注释:

The default concurrency level for this table, used when not otherwise specified in a constructor.

翻译过来的意思:此表的默认并发级别,在构造函数中未另外指定时使用


一开始没搞明白,网上查阅了下,有的博主说这个参数代表ConcurrentHashMap内部的Segment数组的数量,

然后对照着源码,恩,原博说的不错╮(╯▽╰)╭。


我们来看下默认的构造方法源码:

public ConcurrentHashMap(int initialCapacity, floatloadFactor, int concurrencyLevel) {

 

// 参数校验,参数异常直接抛出异常

if (!(loadFactor > 0) || initialCapacity < 0 ||concurrencyLevel <= 0)

throw new IllegalArgumentException();

 

// 如果concurrencyLevel超过最大阀值,就使用最大阀值参数

if (concurrencyLevel > MAX_SEGMENTS)

concurrencyLevel = MAX_SEGMENTS;

// Find power-of-two sizes best matching arguments

 

int sshift = 0;

int ssize = 1;

while (ssize <concurrencyLevel) {

++sshift;

ssize <<= 1;

}

 

this.segmentShift = 32 - sshift;

this.segmentMask =ssize - 1;

if (initialCapacity > MAXIMUM_CAPACITY)

initialCapacity = MAXIMUM_CAPACITY;

int c = initialCapacity /ssize;

if (c *ssize < initialCapacity)

++c;

int cap = MIN_SEGMENT_TABLE_CAPACITY;

while (cap < c)

cap <<= 1;

 

// create segments and segments[0]

Segment<K, V> s0 = new Segment<K, V>(loadFactor, (int) (cap *loadFactor), (HashEntry<K, V>[]) new HashEntry[cap]);

Segment<K, V>[] ss = (Segment<K, V>[]) new Segment[ssize];

UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]

this.segments = ss;

}


从上面这个构造方法源码可以看出,Segment数组的长度ssize是通过concurrencyLevel计算出来的。

 

concurrencyLevel一经指定,不可改变,后续如果ConcurrentHashMap的元素增加导致需要扩容,ConcurrentHashMap不会增加Segment数组的数量。

只会增加Segment中链表数组的容量大小,这样的好处就是扩容不需要对整个ConcurrentHashMaprehash

而只需要对Segment里面的元素做一次rehash就好。

 

整个ConcurrentHashMap初始化过程大致如下:

  1. 先根据concurrencyLevel计算出ssize,然后根据ssize创建Segment数组
  2. Segment数组的数量(长度)是不大于concurrencyLevel的,并且是2的幂次;也就是说segment数组的数量(长度)永远是2的幂次,

这样方便移位操作计算hash,加快hash过程。

  1. 根据initalCapacity和loadFactor确定每个segment容量的大小,每一个segment的容量也是2的指数,同样为了加快hash的过程。

 

简单说下concurrencyLevelssize的关系:

Segment数组的大小ssize是由concurrentLevel来决定的,但是却不一定concurrentLevel

ssize一定是大于或等于concurrentLevel的最小的2的幂次。

举个栗子:

默认情况下,concurrentLevel16,则ssize16

concurrentLevel14ssize16

concurrentLevel17ssize32

为什么Segment的数组大小一定是2的幂次呢?

主要是便于通过按位与的散列算法来定位Segmentindex

 

 

参数initialCpacityConcurrentHashMap的初始容量;

loadfactor是每个segment的负载因子,在构造方法中通过这两个参数来初始化每个segment

具体方法暂时放着..

参考资料:
http://blog.csdn.net/dingji_ping/article/details/51005799#

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值