Contents
  1. 1. 什么是线程安全
    1. 1.1. 正确性
    2. 1.2. 同步
  2. 2. 常用的线程安全场景
  3. 3. 并发下的ArrayList
  4. 4. 并发下诡异的HashMap
  5. 5. 参考链接

什么是线程安全

正确性

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

同步

在线程安全类中封装了必要的同步机制,因此客户端无须进一步采用同步措施。

常用的线程安全场景

  1. 无状态的一定是线程安全的。这个很好理解,因为所谓线程不安全也就是一个线程修改了状态,而另一个线程的操作依赖于这个被修改的状态。
  2. 只有一个状态,而且这个状态是由一个线程安全的对象维护的,那这个类也是线程安全的。比如你在数据结构里只用一个AtomicLong来作为计数器,那递增计数的操作都是线程安全的,不会漏掉任何一次计数,而如果你用普通的long做++操作则不一样,因为++操作本身涉及到取数、递增、赋值 三个操作,某个线程可能取到了另外一个线程还没来得及写回的数就会导致上一次写入丢失。
  3. 有多个状态的情况下,维持不变性(invariant)的所有可变(mutable)状态都用同一个锁来守护的类是线程安全的。这一段有些拗口,首先类不变性的意思是指这个类在多线程状态下能正确运行的状态,其次用锁守护的意思是所有对该状态的操作都需要获取这个锁,而用同一个锁守护的作用就是所有对这些状态的修改实际最后都是串行的,不会存在某个操作中间状态被其他操作可见,继而导致线程不安全。所以这里的关键在于如何确定不变性,可能你的类的某些状态对于类的正确运行是无关紧要的,那就不需要用和其他状态一样的锁来守护。因此我们常可以看到有的类里面会创建一个新的对象作为锁来守护某些和原类本身不变性无关的状态。

上面这三种只是一种归纳,具体到实际应用时,要看你的类哪些状态是必须用锁来守护的,灵活变通。

并发下的ArrayList

ArrayList是一个线程不安全的容器,具体表现请看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ArrayListMultiThread {
static ArrayList<Integer> al = new ArrayList<Integer>(10);
public static class AddThread implements Runnable{
@Override
public void run(){
for(int i = 0; i < 1000000; i++){
al.add(i);
}
}
}
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(new AddThread());
Thread t2 = new Thread(new AddThread());
t1.start();
t2.start();
t1.join();t2.join();
System.out.println(al.size());
}
}

上述代码实现的是采用t1和t2两个线程向一个ArrayList容器中添加元素,最后我们期望可以有2000000个元素在ArrayList中,但事与愿违,你将得到以下三种结果:

  1. 跟你期待的一样,最后打印结果为2000000。

  2. 抛数组越界异常:

    这是由于ArrayList扩容过程中,内部一致性遭到破坏,但由于没有锁的保护,当另一个线程访问到了不一致的内部状态,导致出现越界问题。

  3. 第三种情况是最为头疼的情况,因为它并没有报错:

    1
    1321889

显然这是由于多线程之间的访问冲突,使得保存容器大小的变量被多线程不正常的访问,同时两个线程也同时对ArrayList中的同一个位置进行赋值导致的。

解决方法很简单,只需要将ArrayList换成线程安全的容器vector即可。

并发下诡异的HashMap

HashMap同样不是线程安全的,当你使用多线程访问HashMap时也会遇到意想不到的错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class HashMapMultiThread {
static Map<String, String>map = new HashMap<String, String>();

public static class AddThread implements Runnable {
int start = 0;
public AddThread(int start) {
this.start = start;
}
@Override
public void run() {
for (int i = start; i < 100000; i += 2) {
map.put(Integer.toString(i), Integer.toBinaryString(i));
}
}
}
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(new HashMapMultiThread.AddThread(0));
Thread t2 = new Thread(new HashMapMultiThread.AddThread(1));
t1.start();
t2.start();
t1.join();t2.join();
System.out.println(map.size());
}
}

如果运行正常会打印100000,但是你可能得到以下两种意外结果:

  1. 程序正常结束,但得到一个比100000小的数字,比如95868。

  2. 程序永远无法结束,这在JDK8中已经不存在,详细原因可以查看此传送门

  3. 抛异常:

异常的大概意思HashMap.Node与HashMap.TreeNode进行了异常映射。

stackoverflow有两个回答解释得比较好:

As you can see Java 8’s HashMap has a method called treeify to improve internal storage. Since you’re not using the map in a threadsafe way (as the others already commented) one thread relies on an entry being of class TreeNode while the other most probably changed the same reference to an entry which was of class Node (both extend Map.Entry). – Thomas

I also found the same exception with your code. I added a synchronized modifier on the putEntriesToMap() method, and the error seemed to stop occurring. The problem is that both threads are modifying the same map at once. There is an object that must be converted to put the entry in. However, the second thread is dealing with a mutated object, which throws a ClassCastException. So, make sure that no two threads are accessing the same map at once. The synchronized modifier stops all other threads from doing anything with the class/instance if another thread is doing the same. Synchronized static methods synchronize the class itself, whereas synchronized non-static methods only synchronize the instance of the class.–HyperNeutrino

JDK7中HashMap采用的是位桶+链表的方式。而JDK8中采用的是位桶+链表/红黑树的方式,当某个位桶的链表的长度超过8的时候,这个链表就将转换成红黑树。链表转换红黑树在treeify方法里实现。Node和TreeNode都继承自Map.Entry这个内部接口,所以若map在线程非安全的情况下进行操作,一个线程依赖TreeNode这个类的接口进行存储,而另一个线程又在相同的位置采用Node类的接口进行修改,则会产生如上错误。

参考链接

  1. https://coolshell.cn/articles/9606.html
  2. https://stackoverflow.com/questions/29967401/strange-hashmap-exception-hashmapnode-cannot-be-cast-to-hashmaptreenode/29971168
  3. https://www.jianshu.com/p/4177dc15d658
  4. https://www.zhihu.com/question/26595480
Contents
  1. 1. 什么是线程安全
    1. 1.1. 正确性
    2. 1.2. 同步
  2. 2. 常用的线程安全场景
  3. 3. 并发下的ArrayList
  4. 4. 并发下诡异的HashMap
  5. 5. 参考链接