java ConcurrentHashMap 教程

JAVA xmt_herman_gcy 764浏览 0评论
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:xttblog,发送下载链接帮助你免费下载!
本博客日IP超过1800,PV 2600 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog,之前的微信号好友位已满,备注:返现
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领

ConcurrentHashMap具体是怎么实现线程安全的呢,肯定不可能是每个方法加synchronized,那样就变成了HashTable。

集合是编程中最常用的数据结构。而谈到并发,几乎总是离不开集合这类高级数据结构的支持。比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存作为外部文件的副本(HashMap)。这篇文章主要分析jdk1.5的3种并发集合类型(concurrent,copyonright,queue)中的ConcurrentHashMap,让我们从原理上细致的了解它们,能够让我们在深度项目开发中获益非浅。

通过分析Hashtable就知道,synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hash table,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。
有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。

当资源在多线程下共享时会产生一些逻辑问题,这个时候类或者方法会产生不符合正常逻辑的结果,则不是线程安全的。纵观jdk的版本更新,可以看到jdk的开发人员在高并发和多线程下了很大的功夫,尽可能的通过jdk原生API来给开发人员带来最方便最轻松的高并发数据模型,甚至想完全为开发人员解决并发问题,可以看得出来jdk的开发人员确实很用心。但是在大量业务数据的逻辑代码的情况下高并发还是不可避免,也不可能完全通过jdk原生的并发API去解决这些并发问题,开发人员不得不自己去空值在高并发环境下的数据高可用性和一致性。

前面说了jdk原生的API已经有了很多的高并发产品,在java.util.concurrent包下有很多解决高并发,高吞吐量,多线程问题的API。比如线程池ThreadPoolExecutor,线程池工厂Executors,Future模式下的接口Future,阻塞队列BlockingQueue等等。

数据的可见性

concurrentHashMap相信用的人也很多,因为在数据安全性上确实比HashMap好用,在性能上比hashtable也好用。大家都知道线程在操作一个变量的时候,比如i++,jvm执行的时候需要经过两个内存,主内存和工作内存。那么在线程A对i进行加1的时候,它需要去主内存拿到变量值,这个时候工作内存中便有了一个变量数据的副本,执行完这些之后,再去对变量真正的加1,但是此时线程B也要操作变量,并且逻辑上也是没有维护多线程访问的限制,则很有可能在线程A在从主内存获取数据并在修改的时候线程B去主内存拿数据,但是这个时候主内存的数据还没有更新,A线程还没有来得及讲加1后的变量回填到主内存,这个时候变量在这两个线程操作的情况下就会发生逻辑错误。

原子性

原子性就是当某一个线程A修改i的值的时候,从取出i到将新的i的值写给i之间线程B不能对i进行任何操作。也就是说保证某个线程对i的操作是原子性的,这样就可以避免数据脏读。

volatile的作用

Volatile保证了数据在多线程之间的可见性,每个线程在获取volatile修饰的变量时候都回去主内存获取,所以当线程A修改了被volatile修饰的数据后其他线程看到的一定是修改过后最新的数据,也是因为volatile修饰的变量数据每次都要去主内存获取,在性能上会有些牺牲。

措施

HashMap在多线程的场景下是不安全的,hashtable虽然是在数据表上加锁,纵然数据安全了,但是性能方面确实不如HashMap。那么来看看concurrentHashMap是如何解决这些问题的。

concurrentHashMap由多个segment组成,每一个segment都包含了一个HashEntry数组的hashtable, 每一个segment包含了对自己的hashtable的操作,比如get,put,replace等操作(这些操作与HashMap逻辑都是一样的,不同的是concurrentHashMap在执行这些操作的时候加入了重入锁ReentrantLock),这些操作发生的时候,对自己的hashtable进行锁定。由于每一个segment写操作只锁定自己的hashtable,所以可能存在多个线程同时写的情况,性能无疑好于只有一个hashtable锁定的情况。通俗的讲就是concurrentHashMap由多个hashtable组成。

concurrentHashMap 源码

看下concurrentHashMap的remove操作

V remove(Object key, int hash, Object value) {
	lock();//重入锁
	try {
		int c = count - 1;
		HashEntry<K,V>[] tab = table;
		int index = hash & (tab.length - 1);
		HashEntry<K,V> first = tab[index];
		HashEntry<K,V> e = first;
		while (e != null && (e.hash != hash || !key.equals(e.key)))
			e = e.next;

		V oldValue = null;
		if (e != null) {
			V v = e.value;
			if (value == null || value.equals(v)) {
				oldValue = v;
				// All entries following removed node can stay
				// in list, but all preceding ones need to be
				// cloned.
				++modCount;
				HashEntry<K,V> newFirst = e.next;
				for (HashEntry<K,V> p = first; p != e; p = p.next)
					newFirst = new HashEntry<K,V>(p.key, p.hash,
												  newFirst, p.value);
				tab[index] = newFirst;
				count = c; // write-volatile
			}
		}
		return oldValue;
	} finally {
		unlock();//释放锁
	}
}

Count是被volatile所修饰,保证了count的可见性,避免操作数据的时候产生逻辑错误。segment中的remove操作和HashMap大致一样,HashMap没有lock()和unlock()操作。

看下concurrentHashMap的get源码

V get(Object key, int hash) {
	if (count != 0) { // read-volatile
		HashEntry<K,V> e = getFirst(hash);
	  //如果没有找到则直接返回null
		while (e != null) {
			if (e.hash == hash && key.equals(e.key)) {
	      //由于没有加锁,在get的过程中,可能会有更新,拿到的key对应的value可能为null,需要单独判断一遍
				V v = e.value;
	      //如果value为不为null,则返回获取到的value
				if (v != null)
					return v;
				return readValueUnderLock(e); // recheck
			}
			e = e.next;
		}
	}
	return null;
}

关于concurrentHashMap的get的相关说明已经在上面代码中给出了注释,这里就不多说了。

看下concurrentHashMap中的put

public V put(K key, V value) {
	if (value == null)
		throw new NullPointerException();
	int hash = hash(key.hashCode());
	return segmentFor(hash).put(key, hash, value, false);
}

可以看到concurrentHashMap不允许key或者value为null

接下来看下segment的put

V put(K key, int hash, V value, boolean onlyIfAbsent) {
	lock();
	try {
		int c = count;
		if (c++ > threshold) // ensure capacity
			rehash();
		HashEntry<K,V>[] tab = table;
		int index = hash & (tab.length - 1);
		HashEntry<K,V> first = tab[index];
		HashEntry<K,V> e = first;
		while (e != null && (e.hash != hash || !key.equals(e.key)))
			e = e.next;

		V oldValue;
		if (e != null) {
			oldValue = e.value;
			if (!onlyIfAbsent)
				e.value = value;
		}
		else {
			oldValue = null;
			++modCount;
			tab[index] = new HashEntry<K,V>(key, hash, first, value);
			count = c; // write-volatile
		}
		return oldValue;
	} finally {
		unlock();
	}
}

同样也是加入了重入锁,其他的基本和HashMap逻辑差不多。值得一提的是jdk8中添加的中的putval,这里就不多说了。

总结

ConcurrentHashmap将数据结构分为了多个Segment,也是使用重入锁来解决高并发,讲他分为多个segment是为了减小锁的力度,添加的时候加了锁,索引的时候没有加锁,使用volatile修饰count是为了保持count的可见性,都是jdk为了解决并发和多线程操作的常用手段。

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加QQ1群:135430763(2000人群已满),QQ2群:454796847(已满),QQ3群:187424846(已满)。QQ群进群密码:xttblog,想加微信群的朋友,之前的微信号好友已满,请加博主新的微信号:xttblog,备注:“xttblog”,添加博主微信拉你进群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作可添加助理微信进行沟通!

本文原文出处:业余草: » java ConcurrentHashMap 教程