很多神秘的东西其实早已存在,只要我们善于寻找。-----Hacker Dore

Java平台类库包含了丰富的并发基础构建模块,例如线程安全的容器类以及用于各种协调多个相互协作的线程控制流的同步工具类。

一.同步容器类

Java中的普通容器包uitl中的同步容器类有:Vector,Hashtable,Stack。

我们还可以通过Collections类来获得相应的安全的容器类:这些类实现线程安全的方式是将它们的状态封装起来,并对每个公有方法都进行同步,使得每一次只有一个线程能访问容器的状态。

1.同步容器类的问题
同步容器类虽然都是安全的,但在某些情况下可能需要额外的客户端加锁来保护复合操作。

常见的复合操作有:迭代(遍历容器完中所有元素) ,跳转(根据指定顺序找到当前元素的下一个元素),条件运算(例如:若没有则添加)。

在同步容器类中,这些复合的操作在没有客户端加锁的情况下是线程安全的,但是当多个线程并发地从外部修改容器时,可能会发生不安全的现象。
例如:下面代码给出Vector中定义两个方法

public static Object getLast(Vector list){
  int lastIndex==list.size()-1;
  return list.get(lastIndex);
}

public static void deleteLast(Vector list){
   int lastIndex=list.size()-1;
   list.remove(lastIndex);

}

上面的两个方法在Vector类中的安全性是没有问题的,但是如果我们有两个线程同时分别调用这两个方法,就可能会发生越界访问异常的错误。

所以我们也要保证,多个线程外部调用安全类时也能保持正确性。
对于上面的代码我们可以做如下改进:

public static Object getLast(Vector list){
  synchronized(list){
  int lastIndex==list.size()-1;
  return list.get(lastIndex);
  }
}

public static void deleteLast(Vector list){
   synchronized(list){
   int lastIndex=list.size()-1;
   list.remove(lastIndex);
   }
}

上面通过外部加锁来保证线程安全性。

但还有一种情况我们应该要考虑到,遍历操作如果没有外部加锁也会出现安全问题:

for(int i=0;i<vector.size();i++){
   doSomething(vector.get(i));
}

如果上面的操作和deleteLast同时发生,那么这里也会发生越界异常。

也许你会想,加个锁不就行了:

synchronized(vector){
  for(int i=0;i<vector.size();i++){
   doSomething(vector.get(i));
}

但是,我们要清楚,这样会很大程度上降低了并发性能,这与我们的高并发编程思想矛盾,我们把这个问题先留着。(记作问题1)

2.迭代器(Iterator)和ConcurrentModificationException

首先我们来详细的学习什么是迭代器:
Iterator接口位于java.util包中,包含4个方法:

public interface Iterator<E>{
  E next();//返回下一元素
  boolean hasNext();//判断是否还有下一个元素
  void remove();//从底层集合中删除此迭代器返回的最后一个元素(可选操作)。
  default void forEachRemaining(Consumer<? super E> action);//对每个剩余元素执行给定的操作,
                                                             //直到所有元素都被处理或动作引发异常。
}

通过反复调用next方法,可以逐个访问集合中的每个元素,但是,如果到达了集合的末尾,next方法就会抛出NoSuchElementException异常,因此我们需要在调用next之前调用hasNext来检查是否有下一元素来避免抛出异常,如果hasNext返回true表示有下一个元素。

如果想要查看集合中的所有元素,就请求一个迭代器:

import java.util.ArrayList;
import java.util.Iterator;

public class Text {
    public static void main(String[] args) {
        ArrayList c=new ArrayList();
        for(int i=0;i<5;i++){
            c.add(i);
        }

        Iterator iter=c.iterator();
        while(iter.hasNext()){
            System.out.println(iter.next());
        }
    }
}

在这里插入图片描述
for each循环其实就是通过迭代器来实现的,故for each循环可以处理任何实现了Iterable接口的对象

回到正题,无论直接使用迭代器还是通过for each来对容器进行迭代的标准方式都是使用Iterator,然而如果其他线程并发地修改容器,那么即使是使用迭代器也无法避免在迭代期间对容器加锁。

迭代器表现出的行为是“及时失败(fail-fast)”,这就是说当它发现容器在迭代过程中被修改时,就会抛出一个ConcurrentModificationException,然而这种及时失败的迭代器并不是一种完备的处理机制,它只是善意的捕获并发错误。其实现方式是:将计数器的变化和容器关联起来,如果迭代期间计数器被修改,那么hasNext或next将抛出ConcurrentModificationException,但是这里的检查计数器是在没有同步的情况下进行的,因此可能会看到失效的计数值,而迭代器可能并没有意识到已经发生了修改。(这也是一种权衡吧,从而减低了并发修改操作的检测代码对程序性能带来的影响)

从现在来看,想要避免ConcurrentModificationException,就必须在迭代过程持有容器的锁,
然而有时候我们并不希望在迭代期间对容器加锁(由于加锁迭代器会会降低并发性能,这就对应上面的问题1了),那么我们如何解决呢?
解答问题1:
如果我们不希望再迭代期间对容器加锁,那么一种替代方法就是 “克隆” 容器,并在副本上进行迭代,由于副本被封闭在线程内,因此其他线程不会在迭代期间对其进行修改,这样就避免了抛出ConcurrentModificationException, 但在克隆的过程中仍然要对容器加锁。

3.隐藏迭代器
在某些情况下,迭代器会隐藏起来,例如:

public class HiddenIterator{
  private final Set<Integer> set=new HashSet<Integer>();
  public synchronized void add(Integer i){
     set.add(1);
  }
  public synchronized void remove(Integer i){
    set.remove(i);
  }
  public void addTenThings(){
    Random r=new Random();//新建一个产生随机数的对象
    for(int i=0;i<10;i++){
      add(r.nextInt());
    }
    System.out.println("DEBUG:added ten elememts to "+set);//隐藏了迭代器
  }
  public static void main(String[] args) {
       new HiddenInterator().addTenThings();
    }
}

上面的结果为:
在这里插入图片描述
我们可以知道,在后面输出的语句中的对set进行toSting的操作会对容器进行迭代,这里的迭代器就被隐藏起来了。

注意:正如封装对象的状态有助于维持不变性一样,封装对象的同步机制同样有助于确保实施同步策略。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐