同步容器:可以简单地理解为通过synchronized来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如Vector,Hashtable,以及Collections.synchronizedSet,synchronizedList等方法返回的容器。这些类实现线程安全的方式是:将他们的状态封装起来,并对每个公有的方法都进行同步,使得每次只有一个线程能访问容器的状态。
同步容器都是线程安全的,但是在某些情况下我们可能需要额外的客户端加锁来保护复合操作。容器上常见的符合操作包括:迭代,跳转,以及条件运算。在同步容器类的内部,这些复合操作时线程安全的,但是当其他线程并发的修改容器时,他们可能会有意外的行为。
public static Object getLast(Vector list) { int lastIndex = list.size() - 1; return list.get(lastIndex);
} <span style="font-family: Helvetica, Tahoma, Arial, sans-serif; line-height: 25.2px;"> </span><pre name="code" class="java">public static void deleteLast(Vector list) { int lastIndex = list.size() - 1; list.remove(lastIndex);
}
虽然上面的方法看起来没有问题,Vector自身的方法也是同步的,但是在多线程环境中还是隐藏着问题。如果有两个线程A,B同时调用上面的两个方法,假设list的大小为10,这里计算得到的lastIndex为9,线程B首先执行了删除操作(多线程之间操作执行的不确定性导致),而后线程A调用了list.get方法,这时就会发生数组越界异常。导致问题的原因就是上面的复合操作不是原子操作,这里可以通过在方法内部使用list对象锁来实现原子操作。
解决的方法一:使用客户端加锁的机制,通过过得容器类的锁,我们可以使得如下的两个方法称为原子类操作。但是该方法会降低并发性,牺牲了伸缩性。
<pre name="code" class="java">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); }
}
同步容器将所有对容器的访问都串行化,以实现他们的线程安全性,这种方法的代价是严重降低并发性,当多个线程竞争访问容器的锁时,吞吐量将会严重减低。
并发容器:针对多个线程设计的,用并发容器来代替同步容器,可以极大地提高伸缩性并降低风险。 如ConcurrentHashMap,CopyOnWriteArrayList等。并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在ConcurrentHashMap中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问map,并且执行读操作的线程和写操作的线程也可以并发的访问map,同时允许一定数量的写操作线程并发地修改map,所以它可以在并发环境下实现更高的吞吐量。