打印如下:
因为并发带来了内部错误。
而对于并发容器来讲比如vector和collections.synchronizedlist()就不会出现这个问题。
vector是用方法锁来实现的, 而后者是用同步块来实现的,因此后者的效率较高。
同步容器的单个方法都是安全的,比如上面的那个例子改为使用同步容器:
会打印2000.不会出现异常
但是通常对于容器的操作还会有很多复合操作,比如迭代、缺少才加入等操作,还是会出现问题,这时候需要加入额外的锁。
复合操作:
该代码会抛出:
exception in thread "main" java.util.concurrentmodificationexception
因为遍历的时候会实时检查集合的数量是否发生变化,如果有另外一个线程修改了集合数量则会抛出这个异常。
如果放开代码中的同步块,则不会再抛出异常了。
为了解决这个问题,除了使用加锁的方式外,还可以在遍历之前进行拷贝。
对于这些复合操作,jdk提供了许多类库,提供了比客户端加锁更好的并发性和可伸缩性。
concurrenthashmap
提供了putifabsent等方法提供了一些常用复合操作的并发安全方法。
其实现的机制使用了分离锁, 先hash key到每一个桶上,然后对单独的桶加锁,这样就能够把锁的消耗分解得很小。
同样提供了很多常用复合操作的并发安全方法。
其实现的机制可以看看如下两个方法:
这样所有的可能修改集合的方法都是加了锁的,在修改的时候创建了新的集合,永远不会修改老的集合。
而不会修改集合的地方,比如遍历集合是直接返回了一个当前的数组引用,这个引用不会被修改,因为修改行为会创建新的数组来给引用赋值。
这样很适用于写少读多的情况。
前面说过使用wait和notifyall来实现生产者消费者模式。 这里我们有更好的集合可以使用 blockingqueue
这里如果使用非安全的队列,出了无法实现阻塞效果外,还会造成,多线程写丢失,内部状态不一致,甚至抛出边界异常等等
对于队列,还提供了如下的方法:
add offer put 添加一个元素, 如果满了抛出异常,返回false, 阻塞(仅bockingqueue支持)
remove poll take 取第一个元素并删除 , 如果集合为空,抛出异常,返回null ,阻塞(仅bockingqueue支持)
element peek 返回头元素, 如果为空 抛出异常, 返回null.
出了上面这种外jdk还根据需求提供了别的,比如priorityqueue 根据compare方法来决定取出顺序的队列
deque和blockingdeque双向队列,每个消费者有自己的双端队列,自己的队列完成之后会尝试去消费其他的队列。