从put操作看ConcurrentHashMap如何解决线程安全问题
hashmap的线程不安全性
hashmap(jdk1.8)是线程不安全的,其中一个体现是put
方法,我们看一下大概的逻辑:
如果原来table
这里没有相同hash值的节点,那就new一个,其实就是说,这是一个新节点,没有hash冲突发生。
假设线程A和线程B都执行put
操作,然后key的hash值一样,然后同时走到
if ((p = tab[i = (n - 1) & hash]) == null)
假设之前没有hash相同的节点存在,那么A和B线程就会都执行
tab[i] = newNode(hash, key, value, null);
如此一来后执行的线程就会将先执行的覆盖掉,本来两个元素就变成了一个元素,而不会出现链表挂下去的情况。
我的测试代码:
public class TestHashMapThreadSafe {
public static void main(String[] args) throws InterruptedException {
HashMap<Integer, String> map = new HashMap<>();
Thread thread1 = new Thread(() -> {
map.put(1, "a");
}, "A");
thread1.start();
Thread thread2 = new Thread(() -> {
map.put(17, "b");
}, "B");
thread2.start();
Thread.sleep(1000 * 5);
System.out.println(map);
}
}
为了让效果出来,我需要修改一下put
的源码:
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){
if(key instanceof Integer){
Integer intKey = (Integer) key;
if(intKey == 1 || intKey == 17){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " with key==" + key + " try to new node");
}
}
tab[i] = newNode(hash, key, value, null);
}
...
我让hash冲突的线程睡1秒,让他们同时去newNode
。
打印效果:
A with key==1 try to new node
B with key==17 try to new node
{17=b}
这样最后只有一个元素,显然是有问题的。
ConcurrentHashMap的解决方案
那么,线程安全的ConcurrentHashMap是如何解决上面这个问题呢?
他用了cas和自旋。自旋就是外面那个for循环,失败了就重试直到退出。
我的测试代码:
public class TestConcurrentHashMap {
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();
Thread thread1 = new Thread(() -> {
map.put(1, "a");
}, "A");
thread1.start();
Thread thread2 = new Thread(() -> {
map.put(17, "b");
}, "B");
thread2.start();
Thread.sleep(1000 * 5);
System.out.println(map);
}
}
源码打印的日志:
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
if(key instanceof Integer){
Integer intKey = (Integer) key;
if(intKey == 1 || intKey == 17){
System.out.println(Thread.currentThread().getName() + " enter for loop...");
}
}
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if(key instanceof Integer){
Integer intKey = (Integer) key;
if(intKey == 1 || intKey == 17){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " with key==" + key + " try to new node");
}
}
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null))){
if(key instanceof Integer){
Integer intKey = (Integer) key;
if(intKey == 1 || intKey == 17){
System.out.println(Thread.currentThread().getName() + " cas succeeds...");
}
}
break; // no lock when adding to empty bin
}else {
if(key instanceof Integer){
Integer intKey = (Integer) key;
if(intKey == 1 || intKey == 17){
System.out.println(Thread.currentThread().getName() + " cas fails...");
}
}
}
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
...
我们将看到的效果是:A,B两条线程同时进入
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null)
但是只有一个会cas成功,另一个重新进入for循环然后挂在前一个node下面。
打印结果:
A enter for loop...
A enter for loop...
B enter for loop...
B with key==17 try to new node
B cas succeeds...
A with key==1 try to new node
A cas fails...
A enter for loop...
{17=b, 1=a}
A enter for loop...
B enter for loop...
是好理解的,可是为什么会多一个A enter for loop...
,因为有一个线程要执行map的初始化:
if (tab == null || (n = tab.length) == 0)
tab = initTable();
我们看到了A失败了并重新进入了for循环,这是符合预期的。