之前分析过HashMap的一些实现细节,关于HashMap你需要知道的一些细节, 今天我们从源码角度来看看ConcurrentHashMap是如何实现线程安全的,其实网上这类文章分析特别多,秉着”纸上得来终觉浅,绝知此事要躬行“的原则,我们尝试自己去分析下,希望这样对于ConcurrentHashMap有一个更深刻的理解。
其实ConcurrentHashMap在Android开发中使用的场景并不多,但是ConcurrentHashMap为了支持多线程并发这些优秀的设计却是最值得我们学习的地方,往往”ConcurrentHashMap是如何实现线程安全“这类问题却是面试官比较喜欢问的问题。
首先,我们尝试用代码模拟下HashMap在多线程场景下会不安全,如果把这个场景替换成ConcurrentHashMap会不会有问题。
因为不同于其他的线程同步问题,想模拟出一种场景来表明HashMap是线程不安全的稍微有点麻烦,可能是hash散列有关,在数据量较小的情况下,计算出来的hashCode是不太容易产生碰撞的,网上很多文章都是尝试从源码角度来分析HashMap可能会导致的线程安全问题。
我们来看下下面这段代码,构造一百个线程每个线程都先是往HashMap中put一个数据,然后再将这个数据remove掉,理论上如果没有线程安全问题的话,我们的每个线程拿到的HashMap的size应该都是0.
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class HashMapTest {
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
for (int i = 0; i < 100; i++) {
TestThread testThread = new TestThread(map, "线程名字:" + i);
testThread.start();
}
}
}
class TestThread extends Thread {
public Map<String, String> map;
public String name;
public TestThread(Map<String, String> map, String name) {
this.map = map;
this.name = name;
}
public void run() {
int i = (int)Math.random() * 100;
map.put("键" + i, "值" + i);
map.remove("键" + i);
System.out.println(name + " 当前时间:" + i + " size = " + map.size());
}
}
输出结果如下所示,由于输出量太大,中间删除了部分重复的输出,我们发现打印出的数量是不准确的。
线程名字:0 当前时间:0 size = -1
线程名字:3 当前时间:0 size = -1
线程名字:5 当前时间:0 size = -1
线程名字:7 当前时间:0 size = -1
线程名字:6 当前时间:0 size = -1
线程名字:4 当前时间:0 size = 0
线程名字:9 当前时间:0 size = -1
线程名字:2 当前时间:0 size = -1
线程名字:1 当前时间:0 size = -1
线程名字:10 当前时间:0 size = -1
线程名字:11 当前时间:0 size = -1
线程名字:8 当前时间:0 size = -1
线程名字:12 当前时间:0 size = -1
线程名字:13 当前时间:0 size = -1
...
线程名字:90 当前时间:0 size = -1
线程名字:91 当前时间:0 size = -1
线程名字:92 当前时间:0 size = -1
线程名字:93 当前时间:0 size = -1
线程名字:94 当前时间:0 size = -1
线程名字:95 当前时间:0 size = -1
线程名字:96 当前时间:0 size = -1
线程名字:97 当前时间:0 size = -1
线程名字:98 当前时间:0 size = -1
线程名字:99 当前时间:0 size = -1
那我们如果把这里的HashMap换成ConcurrentHashMap来试试看看效果如何。
public class HashMapTest {
public static void main(String[] args) {
Map<String, String> map = new ConcurrentHashMap<String, String>();
for (int i = 0; i < 100; i++) {
TestThread testThread = new TestThread(map, "线程名字:" + i);
testThread.start();
}
}
}
输出如下,同样中间也省略了很多重复的打印,我们发现这个时候所有的打印的map的size都是0,从而也验证了ConcurrentHashMap是线程安全的。
线程名字:0 当前时间:0 size = 0
线程名字:7 当前时间:0 size = 0
线程名字:3 当前时间:0 size = 0
线程名字:9 当前时间:0 size = 0
线程名字:6 当前时间:0 size = 0
线程名字:2 当前时间:0 size = 0
线程名字:1 当前时间:0 size = 0
线程名字:5 当前时间:0 size = 0
线程名字:4 当前时间:0 size = 0
线程名字:11 当前时间:0 size = 0
线程名字:10 当前时间:0 size = 0
线程名字:8 当前时间:0 size = 0
...
线程名字:91 当前时间:0 size = 0
线程名字:92 当前时间:0 size = 0
线程名字:93 当前时间:0 size = 0
线程名字:94 当前时间:0 size = 0
线程名字:95 当前时间:0 size = 0
线程名字:96 当前时间:0 size = 0
线程名字:97 当前时间:0 size = 0
线程名字:98 当前时间:0 size = 0
线程名字:99 当前时间:0 size = 0
其实列举上面这个例子只是为了从一个角度来展示下为什么说HashMap线程不安全,而ConcurrentHashMap则是线程安全的,鉴于HashMap线程安全例子比较难列举出来,所有才通过打印size这个角度来模拟了下。
\-----------------------------------------------更新分隔线---------------------------------------------- 评论区中指出了上面程序设计的错误,感谢。我看了上面随机数生成的地方,是有点问题。于是改了后,发现运行结果变得不一样了,是我之前这个地方程序设计的问题,再次感谢评论区大佬们指出问题。 修改后的代码如下:
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
public class HashMapTest {
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
// Map<String, String> map = new ConcurrentHashMap<String, String>();
for (int i = 0; i < 100; i++) {
TestThread testThread = new TestThread(map, "线程名字:" + i);
testThread.start();
}
}
}
class TestThread extends Thread {
public Map<String, String> map;
public String name;
private Random random = new Random();
public TestThread(Map<String, String> map, String name) {
this.map = map;
this.name = name;
}
public void run() {
int i = random.nextInt(1000);
System.out.println("i: " + i);
map.put("键" + i, "值" + i);
map.remove("键" + i);
System.out.println(name + " 当前时间:" + i + " size = " + map.size());
}
}
随即数之前生成的一直是零,是有问题的,这里换成random.nextInt来生成了。这段程序不管使用HashMap还是ConcurrentHashMap打印出的size都是0,所有从这个角度来判断是否线程安全是不太合适的。这里没有直接删掉这部分,也是想给读者朋友们一个提示。
另外,我想到了另一种方法来验证HashMap是线程不安全,而ConcurrentHashMap是线程不安全的。