本文的组织形式如下,主要会介绍到同步容器类,操作系统的并发工具,Java 开发工具包(只是简单介绍一下,后面会有源码分析)。同步工具类有哪些。
下面我们就来介绍一下 Java 并发中都涉及哪些模块,这些并发模块都是 Java 并发类库所提供的。
同步容器类
同步容器主要包括两类,一种是本来就是线程安全实现的容器,这类容器有 Vector、Hashtable、Stack,这类容器的方法上都加了 synchronized
锁,是线程安全的实现。
“Vector、Hashtable、Stack 这些容器我们现在几乎都不在使用,因为这些容器在多线程环境下的效率不高。
还有一类是由 Collections.synchronizedxxx
实现的非线程安全的容器,使用 Collections.synchronized 会把它们封装起来编程线程安全的容器,举出两个例子
- Collections.synchronizedList
- Collections.synchronizedMap
我们可以通过 Collections 源码可以看出这些线程安全的实现
要不为啥要称 Collections 为集合工具类呢?Collections 会把这些容器类的状态封装起来,并对每个同步方法进行同步,使得每次只有一个线程能够访问容器的状态。
其中每个 synchronized xxx
都是相当于创建了一个静态内部类。
虽然同步容器类都是线程安全的,但是在某些情况下需要额外的客户端加锁来保证一些复合操作的安全性,复合操作就是有两个及以上的方法组成的操作,比如最典型的就是 若没有则添加
,用伪代码表示则是
if(a == null){ a = get(); }
比如可以用来判断 Map 中是否有某个 key,如果没有则添加进 Map 中。这些复合操作在没有客户端加锁的情况下是线程安全的,但是当多个线程并发修改容器时,可能会表现出意料之外的行为。例如下面这段代码
public class TestVector implements Runnable{ static Vector vector = new Vector(); static void addVector(){ for(int i = 0;i < 10000;i++){ vector.add(i); } } static Object getVector(){ int index = vector.size() - 1; return vector.get(index); } static void removeVector(){ int index = vector.size() - 1; vector.remove(index); } @Override public void run() { getVector(); } public static void main(String[] args) { TestVector testVector = new TestVector(); testVector.addVector(); Thread t1 = new Thread(() -> { for(int i = 0;i < vector.size();i++){ getVector(); } }); Thread t2 = new Thread(() -> { for(int i = 0;i < vector.size();i++){ removeVector(); } }); t1.start(); t2.start(); } }
这些方法看似没有问题,因为 Vector 能够保证线程安全性,无论多少个线程访问 Vector 也不会造成 Vector 的内部产生破坏,但是从整个系统来说,是存在线程安全性的,事实上你运行一下,也会发现报错。
会出现
如果线程 A 在包含这么多元素的基础上调用 getVector
方法,会得到一个数值,getVector 只是取得该元素,而并不是从 vector 中移除,removeVector
方法是得到一个元素进行移除,这段代码的不安全因素就是,因为线程的时间片是乱序的,而且 getVector 和 removeVector 并不会保证互斥,所以在 removeVector 方法把某个值比如 6666 移除后,vector 中就不存在这个 6666 的元素,此时 getVector 方法取得 6666 ,就会抛出数组越界异常。为什么是数组越界异常呢?可以看一下 vector 的源码
如果用图表示的话,则会是下面这样。
所以,从系统的层面来看,上面这段代码也要保证线程安全性才可以,也就是在客户端加锁
实现,只要我们让复合操作使用一把锁,那么这些操作就和其他单独的操作一样都是原子性的。如下面例子所示
static Object getVector(){ synchronized (vector){ int index = vector.size() - 1; return vector.get(index); } } static void removeVector(){ synchronized (vector) { int index = vector.size() - 1; vector.remove(index); } }
也可以通过锁住 .class
来保证原子性操作,也能达到同样的效果。
static Object getVector(){ synchronized (TestVector.class){ int index = vector.size() - 1; return vector.get(index); } } static void removeVector(){ synchronized (TestVector.class) { int index = vector.size() - 1; vector.remove(index); } }
在调用 size 和 get 之间,Vector 的长度可能会发生变化,这种变化在对 Vector 进行排序时出现,如下所示
for(int i = 0;i< vector.size();i++){ doSomething(vector.get(i)); }
这种迭代的操作正确性取决于运气,即在调用 size 和 get 之间会修改 Vector,在单线程环境中,这种假设完全成立,但是再有其他线程并发修改 Vector 时,则可能会导致麻烦。
我们仍旧可以通过客户端加锁的方式来避免这种情况
synchronized(vector){ for(int i = 0;i< vector.size();i++){ doSomething(vector.get(i)); } }
这种方式为客户端的可靠性提供了保证,但是牺牲了伸缩性,而且这种在遍历过程中进行加锁,也不是我们所希望看到的。
fail-fast
针对上面这种情况,很多集合类都提供了一种 fail-fast
机制,因为大部分集合内部都是使用 Iterator 进行遍历,在循环中使用同步锁的开销会很大,而 Iterator 的创建是轻量级的,所以在集合内部如果有并发修改的操作,集合会进行快速失败
,也就是 fail-fast
。当他们发现容器在迭代过程中被修改时,会抛出 ConcurrentModificationException
异常,这种快速失败不是一种完备的处理机制,而只是 善意
的捕获并发错误。
如果查看过 ConcurrentModificationException 的注解,你会发现,ConcurrentModificationException 抛出的原则由两种,如下
造成这种异常的原因是由于多个线程在遍历集合的同时对集合类内部进行了修改,这也就是 fail-fast 机制。
该注解还声明了另外一种方式
这个问题也是很经典的一个问题,我们使用 ArrayList 来举例子。如下代码所示
public static void main(String[] args) { List<String> list = new ArrayList<>(); for (int i = 0 ; i < 10 ; i++ ) { list.add(i + ""); } Iterator<String> iterator = list.iterator(); int i = 0 ; while(iterator.hasNext()) { if (i == 3) { list.remove(3); } System.out.println(iterator.next()); i ++; } }
该段代码会发生异常,因为在 ArrayList 内部,有两个属性,一个是 modCount
,一个是 expectedModCount
,ArrayList 在 remove 等对集合结构的元素造成数量上的操作会有 checkForComodification
的判断,如下所示,这也是这段代码的错误原因。
fail-safe
fail-safe
是 Java 中的一种 安全失败
机制,它表示的是在遍历时不是直接在原集合上进行访问,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 ConcurrentModificationException。java.util.concurrent
包下的容器都是安全失败的,可以在多线程条件下使用,并发修改。
比如 CopyOnWriteArrayList
, 它就是一种 fail-safe 机制的集合,它就不会出现异常,例如如下操作
List<Integer> integers = new CopyOnWriteArrayList<>(); integers.add(1); integers.add(2); integers.add(3); Iterator<Integer> itr = integers.iterator(); while (itr.hasNext()) { Integer a = itr.next(); integers.remove(a); }
CopyOnWriteArrayList 就是 ArrayList 的一种线程安全的变体,CopyOnWriteArrayList 中的所有可变操作比如 add 和 set 等等都是通过对数组进行全新复制来实现的。