Java中的ConcurrentHashMap
ConcurrentHashMap在Java 5中引入了其他并发util,例如CountDownLatch,CyclicBarrier和BlockingQueue。
Java中的ConcurrentHashMap与Hashtable非常相似,但它提供了更好的并发级别。
我们可能知道,我们可以使用集合同步HashMap。
SynchronizedMap(Map)。
因此,ConcurrentHashMap和Collections.synchronizedmap(map)之间有什么区别在Collections.synchronizedmap(map),它锁定整个Hashtable对象,但在ConcurrentHashMap中,它只锁定其中的一部分。
你会在以后的一部分理解它。
另一个区别是,如果我们尝试在迭代时修改ConcurrentHashMap,则ConcurrentHashMap不会抛出并发映射异常。
让我们来一个非常简单的例子。
我有一个国家程序,我们将使用国家类对象作为键及其首都名称(String)作为价值。
下面的示例将了解这些键值对如何存储在ConcurrentHashMap中。
Java中的ConcurrentHashMap示例:
1.国家.java.
package org.arpit.theitroad; public class Country { String name; long population; public Country(String name, long population) { super(); this.name = name; this.population = population; } public String getName() { return name; } public void setName(String name) { this.name = name; } public long getPopulation() { return population; } public void setPopulation(long population) { this.population = population; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } @Override public boolean equals(Object obj) { Country other = (Country) obj; if (name.equalsIgnoreCase((other.name))) return true; return false; } }
- concurrenthashmapstructure.java(主类)
import java.util.HashMap; import java.util.Iterator; public class ConcurrentHashMapStructure { /** * @author Arpit Mandliya */ public static void main(String[] args) { Country San Franceco=new Country("San Franceco",1000); Country japan=new Country("Japan",10000); Country france=new Country("France",2000); Country russia=new Country("Russia",20000); ConcurrentHashMap<country,String> countryCapitalMap=new ConcurrentHashMap<country,String>(); countryCapitalMap.put(San Franceco,"Delhi"); countryCapitalMap.put(japan,"Tokyo"); countryCapitalMap.put(france,"Paris"); countryCapitalMap.put(russia,"Moscow"); Iterator countryCapitalIter=countryCapitalMap.keySet().iterator();//put debug point at this line while(countryCapitalIter.hasNext()) { Country countryObj=countryCapitalIter.next(); String capital=countryCapitalMap.get(countryObj); System.out.println(countryObj.getName()+"----"+capital); } } }
现在将调试点放在第23行,并右键单击"项目 - >调试" - > Java应用程序。
程序将在第23行停止执行,然后右键单击CountryCapitalMap,然后选择Watch.You将能够看到如下结构。
现在从上图中,我们可以观察以下要点
- 有一个被称为段的段[]数组,其具有大小16.
- 它有两个称为semmentshift和segmentMask的变量。
- 此部分存储段类对象。 concurrenthashmap类有一个名为段的内部类
/** * Segments are specialized versions of hash tables. This * subclasses from ReentrantLock opportunistically, just to * simplify some locking and avoid separate construction. */ static final class Segment<K,V> extends ReentrantLock implements Serializable { /** * The per-segment table. */ transient volatile HashEntry<K,V>[] table; //other methods and variables }
现在让我们展开索引3存在的段对象。
在上图中,我们可以看到每个段类包含逻辑上的hashmap。
这里的大小:2k> =(容量/段数)它存储一个名为hashentry的类中的键值对,它类似于HashMap中的条目类。
static final class HashEntry<K,V> { final K key; final int hash; volatile V value; final HashEntry<K,V> next; }
当我们说,ConcurrentHashMap只锁的一部分。
它实际上锁定了一段。
因此,如果两个线程在同一康复冲击图中编写不同的段,则它允许在没有任何冲突的情况下写入操作。
所以段仅用于写入操作。
在读取操作的情况下,它允许完整的并发性,并使用volatile变量提供最近更新的值。
现在,当我们了解ConcurrentHashMap的内部结构时,我们将更容易理解PUT功能。
ConcurrencyLevel的概念:
在创建ConcurrentHashMap对象时,我们可以在构造函数中传递ConcurrencyLevel。
ConcurrencyLevel定义"估计要写入ConcurrentHashMap的线程数"。
默认ConcurrencyLevel是16.这就是为什么,我们在上面创建了16个段对象ConcururentHashMap。
实际的段数将等于ConcurrencyLevel中定义的下一个功率。
例如:假设我们已定义了ConcurrendyLevel为5,因此8段对象将创建为8 = 2 ^ 3所以3较高位的键将用于查找段的索引另一个示例:我们想要10个线程应该能够要同时访问ConcurrentHashMap,因此我们将ConcurrencyLevel定义为10,因此将创建16个段为16 = 2 ^ 4所以4较高的键将用于查找段的索引
put:
put入口的代码如下:
/** * Maps the specified key to the specified value in this table. * Neither the key nor the value can be null. * * The value can be retrieved by calling the <tt>get</tt> method * with a key that is equal to the original key. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with <tt>key</tt>, or * <tt>null</tt> if there was no mapping for <tt>key</tt> * @throws NullPointerException if the specified key or value is null */ 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); } /** * Returns the segment that should be used for key with given hash * @param hash the hash code for the key * @return the segment */ final Segment segmentFor(int hash) { return segments[(hash >>> segmentShift) & segmentMask]; } //Put method in Segment: V put(K key, int hash, V value, boolean onlyIfAbsent) { lock(); try { int c = count; if (c++ > threshold) //ensure capacity rehash(); HashEntry[] tab = table; int index = hash & (tab.length - 1); HashEntry first = tab[index]; HashEntry 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(key, hash, first, value); count = c; //write-volatile } return oldValue; } finally { unlock(); } }
当我们将任何键值对添加到ConcurrentHashMap时,请执行以下步骤:
- 在ConcurrentHashMap中,密钥不能为null。键的Hashcode方法用于计算哈希码
- Key的Hashcode方法写入不佳,因此Java开发人员添加了一个方法哈希(类似于HashMap),应用了另一个散列()函数并计算哈希码。
- 现在我们需要首先找到段的索引,用于查找给定密钥的段,使用上面的segcer for方法。
- 获取段后,我们使用段的put方法。当将密钥值对放入段中,它获取锁定,因此没有其他线程可以在此块中输入,然后使用哈希&(tab.length-1)找到偏出索引数组中的索引。
- 如果我们密切关注,则段的Put方法类似于HashMap的Put方法。
putifabsent:
我们只想在kecurrenthashmap中放置元素,只有在返回旧值的情况下它没有键。
这可以写作:
if (map.get(key)==null) return map.put(key, value); else return map.get(key);
如果我们使用Purifabsent方法,上面的操作是原子的。
这可能是必需的行为。
让我们在一个例子的帮助下了解:
- 线程1在ConcurrentHashMap中放置值。
- 同时,线程2正在尝试从ConcurrentHashMap读取(获取)值,并且它可能会返回null,因此它可能会覆盖在Concurrenthashmap中放置的任何线程1.
可能不需要上述行为,因此ConcurrentHashMap具有Purifabsent方法。
/* * {@inheritDoc} * * @return the previous value associated with the specified key, * or <tt>null</tt> if there was no mapping for the key * @throws NullPointerException if the specified key or value is null */ public V putIfAbsent(K key, V value) { if (value == null) throw new NullPointerException(); int hash = hash(key.hashCode()); return segmentFor(hash).put(key, hash, value, true); }
get:
/** * Returns the value to which the specified key is mapped, * or {@code null} if this map contains no mapping for the key. * * More formally, if this map contains a mapping from a key * {@code k} to a value {@code v} such that {@code key.equals(k)}, * then this method returns {@code v}; otherwise it returns * {@code null}. (There can be at most one such mapping.) * * @throws NullPointerException if the specified key is null */ public V get(Object key) { int hash = hash(key.hashCode()); return segmentFor(hash).get(key, hash); } /* Specialized implementations of map methods */ //get method in Segment: V get(Object key, int hash) { if (count != 0) { //read-volatile HashEntry<K,V> e = getFirst(hash); while (e != null) { if (e.hash == hash && key.equals(e.key)) { V v = e.value; if (v != null) return v; return readValueUnderLock(e); //recheck } e = e.next; } } return null; }
从concurrenthashmap获取值直截了当。
- 使用键的哈希码计算哈希值
- 使用segment for获取段索引。
- 使用段的获取功能来获取与密钥对应的值。
- 如果它在ConcurrentHashMap中找不到值,它会锁定该段并再次尝试获取该值。
最佳实践:
如果我们需要低级别的并发级别,我们不应使用ConcurrentHashMap的默认构造函数,因为默认的ConcurrencyLevel为16,默认情况下它将创建16个段。
我们应该使用完全参数化构造函数:
ConcurrentHashMap(Int IntentCapacity,Float LoadFactor,INT ConcurrencyLevel)
在上面的构造函数中,IntentCapacity和LoadFactor与HashMap和ConcurrencyLevel相同,与我们上面定义相同。
因此,如果我们只需要两个可以同时编写的三个线程,则可能会将ConcurrentHashMap初始化为:
ConcurrentHashMap ch=new ConcurrentHashMap(16,0.75f,2);
如果我们有很多写线程和大量读取线程,ConcurrentHashMap会更好地执行更好。