并发编程 - 同步容器

摘要

同步容器类 : Vector , Hashtable ,以及Collections.synchronizedXxx等创建的类;

特征:将自身状态封装起来,并对每个共有方法都进行同步,使得每次只有一个线程能访问容器的状态;

关键字

复合操作,迭代器,隐式迭代操作,ConcurrentModificationException

复合操作上的并发问题

程序清单 1 Vector 上可能导致混乱结果的复合操作

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

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

以上两个方法在同一个线程内使用的时候,不会有问题,但是如果分别在两个线程内执行,当getLast方法执行到list.get...时,假设从当前Vector获取的容器大小为5,这时CPU将时间片分配给第二个线程,第二个线程执行deleteLast方法,并完成从list中移除一个元素,那么当前容器的大小则为 4 ,紧接着CPU将时间片重新分配给第一个线程继续执行getLast方法中的list.get(lasteIndex) , list.get(4)就会报ArrayIndexOutOfBoundsException异常。

如何解决这类问题?

程序清单 2 复合操作加锁保证原子性

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

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

容器上的复合操作包含:迭代,跳转,以及条件运算,例如addIfAbsent, 这些操作在其他线程并发修改容器时,可能会表现出意料之外的行为。虽然容器在复合操作下抛出异常,但同步容器依然是线程安全的,例如Vector,其状态仍然有效,而抛出的异常也与其规范保持一致。但是抛出异常显然不是用户所期待的。另外同步容器加锁保证复合操作的原子性降低了并发性,假设在迭代过程中,为了防止其他线程对容器进行修改而对容器加锁,那么如果容器内元素的数量比较大,会导致迭代过程中其他线程无法对其进行访问。

迭代器与ConcurrentModificationException

首先看一个与线程安全/同步无关的问题示例如下:

程序清单 3 迭代过程中修改容器

public class IteratorExceptionTest {
    public static void main(String[] args) {
        List<String> temp = new ArrayList<>();
        temp.addAll(Arrays.asList("1", "2", "3"));
        for (String s : temp) {
            temp.remove(s);
        }
    }
}

以上源码结果输出:

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at com.learn.concurrency.IteratorExceptionTest.main(IteratorExceptionTest.java:11)

将以上执行结果称为“及时失败”(fail-fast) 处理机制,将容器的变化与计数器联系起来,如果在迭代期间计数器被修改,那么hasNext或者next将抛出ConcurrentModificationException。

程序清单 4 使用迭代器修改容器

public class IteratorExceptionTest {
    public static void main(String[] args) {
        List<String> temp = new ArrayList<>();
        temp.addAll(Arrays.asList("1", "2", "3"));
        Iterator<String> iterator = temp.iterator();
        while(iterator.hasNext()){
            String next = iterator.next();
            if("2".equals(next)){
                iterator.remove();
            }
        }
        System.out.println(temp);
    }
}

以上源码结果输出:

[1, 3]

以上结果表明,以迭代器的方法对容器进行修改,将不会抛出异常。其实内部也正是因为迭代器在对容器进行修改时,会同步修改容器的计数器,所以不会抛出异常。

那我们再回头想一下并发处理下,容器的执行效果,其实很自然的会发生一种情况,当容器在迭代过程中,其计数器达到N时,另一个线程通过对容器进行修改,从而将计数器值修改为M,那容器再继续迭代下去,就肯定会抛出异常。

其实这种“及时失败”的迭代器并不是一种完备的处理机制,而只是“善意地”捕获并发错误,因此只能作为并发问题的预警指示器。

如何解决此类问题?

其实这类问题与复合操作中的并发问题类似,都可以采用在迭代过程持有容器的锁来解决,但是缺点是一致的,如果容器的规模很大,或者在每个元素上执行操作的时间很长,其他线程将长时间等待,持有锁的时间越长,那么在锁上的竞争就可能越激烈,如果许多线程都在等待锁被释放,那么将极大地降低吞吐量和CPU的利用率。

那有其他方案吗?可以借鉴其他容器的想法,如"克隆容器" , 在副本上进行迭代,由于副本只会在线程内部进行操作,则不会被其他线程修改,这样就避免抛出异常,但是克隆容器存在显著的性能开销,这种方式的好坏取决于:容器的大小,在每个元素上执行的工作,迭代操作相对于容器其他操作的调用频率,以及在响应时间和吞吐量等方面的需求。

隐式迭代器

程序清单 5 隐式使用迭代器

public class HiddenIterator {
    public static void main(String[] args) {
        List<Integer> integers = Collections.synchronizedList(new ArrayList<>(Arrays.asList(1, 2, 3, 4)));
        System.out.println("list : " + integers);
    }
}

以上程序输出结果可能会抛出ConcurrentModificationException异常,以下为这两句代码的反汇编后的代码:

public class com.learn.concurrency.HiddenIterator {
  public com.learn.concurrency.HiddenIterator();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: iconst_4
       5: anewarray     #3                  // class java/lang/Integer
       8: dup
       9: iconst_0
      10: iconst_1
      11: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      14: aastore
      15: dup
      16: iconst_1
      17: iconst_2
      18: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      21: aastore
      22: dup
      23: iconst_2
      24: iconst_3
      25: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      28: aastore
      29: dup
      30: iconst_3
      31: iconst_4
      32: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      35: aastore
      36: invokestatic  #5                  // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
      39: invokespecial #6                  // Method java/util/ArrayList."<init>":(Ljava/util/Collection;)V
      42: invokestatic  #7                  // Method java/util/Collections.synchronizedList:(Ljava/util/List;)Ljava/util/List;
      45: astore_1
      46: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
      49: new           #9                  // class java/lang/StringBuilder
      52: dup
      53: invokespecial #10                 // Method java/lang/StringBuilder."<init>":()V
      56: ldc           #11                 // String list :
      58: invokevirtual #12                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      61: aload_1
      62: invokevirtual #13                 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      65: invokevirtual #14                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      68: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      71: return
}

从code = 49行开始看,其内部实例化一个StringBuilder,可见其要完成的操作是:"list : " + integers ,在62行对integers进行append的时候,采用了toString方法,标准容器的toString方法将迭代容器,并在每个元素上调用toString来生成容器内容的格式化标识。很明显,如果在容器迭代过程中,被其他线程所修改,就会抛出异常。

程序清单 6 AbstractCollection中的toString方法内部实现

public String toString() {
        Iterator<E> it = iterator();
        if (! it.hasNext())
            return "[]";

        StringBuilder sb = new StringBuilder();
        sb.append('[');
        for (;;) {
            E e = it.next();
            sb.append(e == this ? "(this Collection)" : e);
            if (! it.hasNext())
                return sb.append(']').toString();
            sb.append(',').append(' ');
        }
    }

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

容器的hashCode和equals等方法也会间接地执行迭代操作,当容器作为另一个容器的元素或键值是,就会出现这种情况。同样,containsAll, removeAll和retainAll等方法,以及把容器作为参数的构造函数,都会对容器进行迭代。所有这些间接的迭代操作都有可能抛出ConcurrentModificationException。

小结

同步容器只是对其各个操作进行加锁处理,但是在复合操作下,仍然不能达到原子性,需要自己额外加锁保证线程的安全性。隐式的迭代操作主要做了一些复合操作,这些操作由于非原子性,所以在并发处理中,存在线程安全问题。

转载于:https://my.oschina.net/u/4026254/blog/2885270

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值