这一节来讲一讲java.util.concurrent这个包里的一些重要的线程安全有关类。

synchronized容器

synchronized容器就是把自己的内部状态封装起来,通过把每一个public方法设置成同步来控制对共享变量的访问的容器。主要包括Vector, Hashtable,以及Collections.synchronizedxxx()方法提供的wrapper。

synchronized容器的问题-client locking

首先,synchronzied容器虽然是线程安全的,但是要访问容器内部数据的线程只能先拿到容器的内置锁才能访问,实际上相当于串行访问,CPU利用率和效率都不高。
另外还有一个值得注意的地方,就是用户代码使用synchronized容器时,如果需要做一些复合操作,比如put-if-absent,仍然要显式加锁(称为client locking),否则会产生race condition。
比如以下操作:

复制代码
1 public Object getLast(Vector list){ 2   int last = list.size() - 1; //13   return list.get(last); //24 } 5 public void removeLast(Vector list){ 6   int last = list.size() - 1; //37   list.remove(last); //48 }
复制代码

以上两个方法都对Vector进行了复合操作,在不加锁的情况下可能产生这样一种场景:线程A调用getLast(),同时线程B调用removeLast()。线程A进行step 1拿到last同时线程B也拿到同样的last;此时由于线程调度上的原因,线程B先执行了step 4删除了最后一个节点,而线程A在此之后才执行step 2, 由于最后一个节点已被删除,线程A这里会报ArrayIndexOutOfBoundsException,而这个错误并不是用户希望看到的。




所以如果要按照类似的方法使用synchronized容器的话还是需要自己加锁。由于这些容器内部的线程安全策略是使用自己的内置锁,所以用户代码加锁的时候需要用到的是容器本身。

复制代码
 1 public Object getLast(Vector list){  2   synchronized(list){  3     int last = list.size() - 1; //1 4     return list.get(last); //2 5   }  6 }  7 public void removeLast(Vector list){  8   synchronized(list){  9     int last = list.size() - 1; //310     list.remove(last); //411   } 12 }
复制代码

除了这些用户自定义的复合操作之外,其实iteration也算复合操作,所以也应该加锁。此处应注意两点:

  1.  容器自带的Iterator本身不支持并发修改,所以它提供了一个所谓的fail-fast的并发修改报错机制,即容器自身维护一个modCount域,Iterator在创建时记录这个modCount的值,如果在用户遍历容器的过程中modCount值发生了改变,则说明有另一个线程对容器做出了修改,那么Iterator马上会抛出ConcurrentModificationException。

    这个机制严格意义上并不能够100%地探测到并发修改,因为modCount这个域并不是volatile的,在判断    

复制代码
    if(modCount == expectedModCount)
复制代码

   时也并未加锁。作者描述这个机制是在考虑性能的情况下所做的一个best-effort的努力。总之,不应该对这个机制做过多的依赖。

       2.  有一些容器自带的方法看起来很无辜,但内部会用到iterator,所以用户用到这些无辜方法的时候还是要加锁。比如我们常用的toString, for-each语法,hashCode, equals, containsAll, removeAll, retainAll, 以其他容器为参数的构造器,等等。而这些方法有时候也是被隐式调用的,很难检查到,比如: