二,从ConcurrentHashMap类学习高并发程序的设计思路【深入JDK源码】
ConcurrentHashMap是JDK 1.5推出的类,性能上比HashTable和Collections.synchronizedMap(new HashMap())快很多。
看此类源码最好和HashTable对比理解,会发现它的优化,此类一出HashTable可废。
优化的方向,降低读对锁的依赖,写都是加锁。
一,主要是用了分离锁
1.概括结构如下,可以简单理解成把一个大的HashTable分解成多个,形成了锁分离。
ConcurrentHashMap默认是分离了16个模块,即理想状态下能有16个线程同时并发(指要修改的map处于不同的模块之中)。
采用分离锁可以避免无意义的等待,相比。在HashTable中实现就简单多了,代码如下
public synchronized V put(K key, V value) { ... }
HashTable是直接在方法前加了synchronized关键字,把整块都锁上了
2,应用场景
当有一个大数组时需要在多个线程共享时就可以考虑是否把它给分层多个节点了,避免大锁。并可以考虑通过hash算法进行一些模块定位。
其实不止用于线程,当设计数据表的事务时(事务某种意义上也是同步机制的体现),可以把一个表看成一个需要同步的数组,如果操作的表数据太多时就可以考虑事务分离了(这也是为什么要避免大表的出现),比如把数据进行字段拆分,水平分表等
二,和使用final变量
1, final Segment<K,V>[] segments;
static final class HashEntry<K,V> { final K key; final int hash; volatile V value; final HashEntry<K,V> next; ... ... //全是final }
这样构建列表主要特性是,并发时你读到的值永远不会改变. 所以读的时候并不需要加锁
V get(Object key, int hash) { if (count != 0) { // read-volatile HashEntry<K,V> e = getFirst(hash);//这里取的是table的head,因为table用的是volatile也不需要担心并发remove时重排 while (e != null) { if (e.hash == hash && key.equals(e.key)) { V v = e.value; //当然这里value用的是volatile也是不加锁直接可以的读原因 if (v != null) return v; return readValueUnderLock(e); // recheck 当然这里是防止当修改时或者remove时的 } e = e.next; } } return null; }
这里用的Key和 next都是final根本不需要担心链正在被修改,而链的头部,即table[n]是volatile元素,也不需要担心同步
当然增加了final,相应的方法也不对了,最好的就是对比下和HashTable的remove方法
2, 当有固定结构,如链表,树等结构时可以尝试把指针即(如next)和元素标识(如key ,id)做final处理,可以学下ConcurrentHashMap的remove方法,不过注意它的头是同步的.
三,和volatile变量代替了线程的锁。
在ConcurrentHashMap中,主要定义volatile变量有
transient volatile int count;
transient volatile HashEntry<K,V>[] table;
1. 概括.
在Java Memory Model中一般JVM会优化把变量写入当前线程的高速缓存,然后在在先入共享内存中,而Volatile则要求去掉JVM的优化,直接把变量写入JVM的共享内存中,性能上有很大的损耗,从而也失去了程序的原子性。
写个代码测试证明下,如下
package org.benson.another; /** * * @author BensonHe QQ 277803242 * @blogFrom http://www.cnblogs.com/springsource * TODO Research for volatile variable */ public class Test4Volatile { public volatile int volaCount = 0; public int nonVolaCount = 0; /** * run the same job for a volatile and non-volatile variable * @param loopCount * @return how much the job spend seconds */ public long howLongVolatile(long loopCount) { long startTime = java.util.Calendar.getInstance().getTime().getTime(); for (int i = 0; i < loopCount; i++) volaCount++; return java.util.Calendar.getInstance().getTime().getTime() - startTime; } /** * run the same job for non-volatile variable * @param loopCount * @return how much the job spend seconds */ public long howLongNonVolatile(long loopCount) { long startTime = java.util.Calendar.getInstance().getTime().getTime(); for (int i = 0; i < loopCount; i++) nonVolaCount++; return java.util.Calendar.getInstance().getTime().getTime() - startTime; } public static void main(String[] args) { Test4Volatile test4Volatile = new Test4Volatile(); System.out.println("the volatil variable spend "+test4Volatile.howLongVolatile(100000000l)); System.out.println("the non-volatil variable spend "+test4Volatile.howLongNonVolatile(100000000l)); } }
运行结果如下
the volatil variable spend 1860 the non-volatil variable spend 284
这就是JVM优化的能力
所以要正确的使用variable很重要,看看ConcurrentHashMap中的代码对它的应用,以count为例
V put(K key, int hash, V value, boolean onlyIfAbsent) { lock(); //放进一个同步快中 try { int c = count; //先赋值给一个非volatile变量进行技术 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(); } }
所以,当要对改变量进行计算时,可以把变量赋值到另一个非volatile变量(当然要注意此时计算变量也失去了和共享内存中的一致性,但赋给了原子性了,所以必须放到同步块中增加一致性),进行计算,待计算完成后再赋值给volatile变量,写入共享内存。
2,应用场景推荐
读的操作远远大于写的时候可以用volatile优化,代替同步。
如果只读的话,性能和非volatile都是一样的,并保证一致性
修改代码证明
package org.benson.another; /** * * @author BensonHe QQ 277803242 * @blogFrom http://www.cnblogs.com/springsource * TODO Research for volatile variable */ public class Test4Volatile { public volatile int volaCount = 0; public int nonVolaCount = 0; public int readCount; /** * run the same job for a volatile and non-volatile variable * @param loopCount * @return how much the job spend seconds */ public long howLongVolatile(long loopCount) { long startTime = java.util.Calendar.getInstance().getTime().getTime(); for (int i = 0; i < loopCount; i++) readCount=volaCount; return java.util.Calendar.getInstance().getTime().getTime() - startTime; } /** * run the same job for non-volatile variable * @param loopCount * @return how much the job spend seconds */ public long howLongNonVolatile(long loopCount) { long startTime = java.util.Calendar.getInstance().getTime().getTime(); for (int i = 0; i < loopCount; i++) readCount=nonVolaCount; return java.util.Calendar.getInstance().getTime().getTime() - startTime; } public static void main(String[] args) { Test4Volatile test4Volatile = new Test4Volatile(); System.out.println("the volatil variable spend "+test4Volatile.howLongVolatile(100000000l)); System.out.println("the non-volatil variable spend "+test4Volatile.howLongNonVolatile(100000000l)); } }
输出
the volatil variable spend 286 the non-volatil variable spend 279
可以看到性能几乎一样
附:
Volatile 变量法则:对 Volatile 域的写入操作 happens-before 于每个后续对同一 Volatile 的读操作。
如果正确使用Volatile 可以优化线程,使用Volatile原则如下当对一个变量的读远远大于对其修改操作时可以考虑用Volatile优化了。
欢迎交流, QQ 107966750
参考资料: