同步容器类
常见同步类
Vector
Hashtable
-
Collections.synchronizedXxx工厂方法创建的封装容器
注:Vector和Hashtable是早期JDK的部分,Collections.synchronizedXxx是JDK1.2添加的
同步容器实现线程安全的方式
- 使用自身的锁来保护它的每个方法
- 将他们的状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态
同步容器类的问题
容器上的复合操作存在线程安全问题
-
常见的复合操作:
- 迭代(反复访问元素,直到遍历完容器内的所有元素)
- 跳转(根据指定顺序找到当前元素的下一个元素)
- 条件运算(例如:若没有则添加)
//查询vector.size()状态可能是失效的 for(int i=0; i<vector.size(); i++) { doSomething(vector.get(8)); }
``` /** * 先检查后查询的复合操作 */ public static Object getLast(Vector list) { //查询list.size()的状态可能是失效的 int lastIndex = list.size() - 1; return list.get(lastIndex); } ```
解决复合操作线程不安全的方法
- 客户端在复合操作上加锁(同步容器使用自身的锁来保护它的每个方法)
sychronized(vector) {
for(int i=0; i<vector.size(); i++) {
doSomething(vector.get(8));
}
}
public static Object getLast(Vector list) {
synchronized(list) {
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
}
迭代器与ConcurrentModificationException
-
容器迭代的方式
- 直接使用Iterator
- 使用Java5.0引入的for-each
-
迭代器的问题
- 在同步容器的迭代器是并没有考虑到并发修改的问题,并且他们表现出的行为是“及时失败(fail-fast)”,这意味着,当他们发现容器在迭代过程中被修改时,就会抛出一个ConcurrentModificationException异常
- 这种“及时失败”的迭代器并不是一种完备的处理机制,而只是“善意地”捕获并发错误,因此只能作为并发问题的预警指示
-
解决ConcurrentModificationException问题
-
“克隆”容器,并在副本上进行迭代
注:克隆过程中仍然需要对容器加锁;克隆容器时存在显著的性能开销;这种方式的好坏取决于多个因素,包括容器大小,在每个元素上执行的工作,迭代操作相对于容器其他操作的调用频率,以及在响应时间和吞吐量等方面的需求
-
-
常见隐藏的迭代
容器的hashCode和equals方法会间接执行迭代操作,当容器作为另一个容器的元素或键时就会出现这种情况
containsAll、removeAll和retainAll等方法
-
将容器作为参数的构造函数
注:所有间接的迭代操作都可能抛出ConcurrentModificationException
并发容器
ConcurrentHashMap与Hashtable的区别
- 相同点:都是线程安全的容器
- 不同点
- 同步方式不同:前者分段同步且读取操作不同步,并发性能高;后者全表同步,并发性能低
- 内存消耗:前者占用内存大,因为分段,内部容器较多或占用更多额外内存
- 迭代器一致性:前者迭代器具有弱一致性,因为读取非同步,所以读取的结果只包含创建迭代器时已有的元素;后者因为是全表同步,所以保证了迭代器的强一致性
- 增加API:前者增加了一些对常见复合操作的支持,例如“若没有则添加”、替换以及有条件删除
- 弱化API:前者弱化了size()、isEmpty()方法的语义以反映容器的并发性,这些方法的结果可能是失效的
- 迭代异常:迭代时不会出现ConcurrentModificationException,后者会出现异常
- 并发容器替代同步容器
- ConcurrentHashMap <- HashTable、Collections.synchronizedMap(Map)
- ConcurrentSkipListMap <- Collections.synchronizedMap(TreeMap)
- ConcurrentSkipListSet <- Collections.synchronizedSet(TreeSet)
CopyOnWriteArrayList特点
- 写操作复制内部容器,读操作读取的是内部容器副本,迭代时不会出现ConcurrentModificationException
- 适合于读操作多,写操作少的多线程环境
中断
概念
- 中断是一种用于取消线程操作的协作机制;一个线程不能强制其他线程停止正在执行的操作而去执行其他的操作,但是要求(通知)其他线程执行到某个可以暂停的地方停止正在执行的操作,前提是别的线程愿意停止当前的操作
中断方法介绍
-
isInterruputed 对象方法,获取线程的中断状态
注:线程死了后,该方法始终返回false
-
interrupt 对象方法,设置线程的中断状态
注:如果线程阻塞在Object的wait方法或Thread的join、sleep方法上,调用此方法将清除被阻塞线程的中断状态,并且调用这些方法的地方将收到一个InterruptException
-
interrupted 静态方法,返回当前线程的中断状态,并清除中断状态、
注:线程死了后,该方法始终返回false
中断异常InterruptedException处理
- 传递InterruptException:不捕获该异常,或者捕获该异常然后在执行某种简单的清理工作后再次抛出这个异常
- 恢复中断:有时候不能抛出InterruptException,例如当代码是Runnable的一部分时,这种情况下必须捕获InterruptException,并通过调用当前线程的interrupt方法恢复中断状态,将中断信息告知调用栈中更高层的代码
同步工具类
概念
- 同步工具类可以是任何对象,只要他根据其自身状态来协调线程的控制流;例如阻塞队列可以作为同步工具类
常用同步工具类
- CountDownLatch(闭锁)
- 延迟线程的进度,直到其达到终止状态,所有线程将释放进度,当其到达结束状态后,将不会再改变状态
- FutureTask(也可以当做闭锁)
- 可以返回计算结果的任务
- 包括三种状态:等待运行(Waiting to run)、正在运行(Running)、运行完成(Completed)
- 执行完成表示计算的所有结束方式:正常结束、由与取消结束、由与异常结束;任务进入完成状态后将会停止在这个状态上
- FutureTask表示的计算通过Callable接口来实现
- Callable可以抛出受检查的或未受检查的异常,并且任何代码都可能抛出Error
- 无论任务代码抛出什么异常,都会被封装到一个ExecutionException中,并在Future.get中被重新抛出
- 上面两条将使get代码变得复杂,因为不仅需要处理可能出现的ExecutionException以及未受检查的CancellationException,而且还由于ExecutionException是做为一个Throwable类返回的
- 优点:提前启动计算,可以减少等待结果需要的时间
- Semaphore
- 对资源施加边界
- CyclicBarrier
- 与闭锁相同点:栅栏类似于闭锁,他能阻塞一组线程直到某个事件发生
- 与闭锁不同点:闭锁是一次性的,栅栏可多次使用;闭锁用于等待事件,而栅栏用于等待其他线程
- Exchanger
- 它是一种Two-Party栅栏,各方在栅栏位置上交换数据