2013 1204:
5.1 同步容器类
同步容器类是指使用这种策略进行多线程保护的容器类:访问操作被synchronized修饰,被严格串行化。
比如Vector类和用Collections.synchronizedmap得到的Map。
Vector
Vector类的串行化极其严格,无论是修改动作的add方法,还是读取动作的get、size、elementAt等等方法,都直接由synchronized修饰,并且连迭代器也是如此,迭代方法next、remove都会锁住Vector对象。
Vector的迭代器还有修改动作检查,如果迭代期间有任何其他地方的修改动作,那么就会抛出异常。
get或其他访问方法在越界时,还会抛出越界异常。
为了能够让程序正常运行,就不得不在整个迭代期间上锁,比如: Vector v; synchronized(v) { // 迭代},又或者取对象时进行这样的动作: synchronized(v) {if (a < v.size()) get(a);}
因此这个同步容器在多线程下的功能十分有限。
Collections.synchronizedmap
这个方法得到的Map,会在除迭代以外的其他访问方法上上锁。也就是说,它没有解决迭代期间被修改的问题,一旦被修改就会抛出ConcurrentModificationException。甚至它还不提供客户端去为整个迭代上锁的方式。
可以说,这个方法得到的Map不应该使用迭代。
可是实际上迭代动作,即使不是显示调用,也可能出现。
比如for循环,又比如基本上所有容器的toString方法里面,都会用迭代器去遍历元素。
这种隐藏迭代器,使得Collections.synchronizedmap非常不安全。
5.2 并发容器
并发容器的代表ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue和LinkedBlockingQueue
分段锁:ConcurrentHashMap
迭代时对写动作不敏感,分段存储数据,每次读写只在某个段上进行上锁,降低了锁冲突,适合修改和迭代都很频繁的情况。
事实不可变:CopyOnWriteArrayList
这是一个很特别的实现,它里面使用一个对象数组存储数据。每次有修改动作的时候,都会把原来的数组拷贝到新的数组上面,把新的元素添加或者修改到新的数组里面,然后舍弃掉旧的数组。这样的做法显然增加了修改时的性能损耗,但是它有着很强大的作用——它的迭代都会保存当时的数组(快照),只在这个数组上进行,由于这个数组在形成以后就不会再发生任何变化,所以不会有任何多线程问题。而下一次迭代,又会在新的数组上进行。
总而言之,就是一次迭代保证不变性,多次迭代保证可变性。
这个容器用于修改动作远远少于迭代动作的情况,比如监听器存储。注册和销毁监听器的动作,远远少于事件的数量,迭代上不需要任何锁,性能十分优越。
原子操作委托:ConcurrentLinkedQueue
这个类使用UNSAFE来串行化读写操作,迭代时同样对写动作不敏感。
应用同步工具类:LinkedBlockingQueue
这个类使用了ReentrantLock和Condition来进行offer和poll的控制。
5.3 阻塞队列和生产者-消费者模式
BlockingQueue
BlockingQueue有许多实现,LinkedBlockingQueue、ArrayBlockingQueue和PriorityBlockingQueue,这种阻塞队列使用时,需要注意生产者和消费者的处理速率。开发人员经常会假设消费者的速率会跟得上生产者的,但实际上这常常不被保证。如果生产者的效率更高,那么数据就会堆积在队列中越来越多,导致耗尽内存。
有界队列是一种强大的资源管理工具,它们能通过阻塞生产者的方式,抑制并防止产生过多的生产项,使应用程序在负荷过载的情况下更加健壮。所以习惯性的在LinkedBlockingQueue构造时,加上容量参数吧。
阻塞队列在多线程方面的模式,其实是串行线程封闭,也就是说,一个对象通过一次同步处理(阻塞队列),被一个线程转移到另一个线程里面去。两个线程不存在同时操作该对象的场景。对象一开始封闭在生产者线程中,后来被封闭在消费者线程中。
双端队列与工作密取
双端队列是支持在尾部添加对象,在首尾两端都可以取走对象的队列。工作密取的一个典型例子是多处理器,当一个处理器的任务队列空了以后,它主动出击,从其他处理器的队列尾部获取任务来执行(work stealing)。
5.4 阻塞与中断
阻塞上升:一个方法调用了阻塞方法,那么本身就变成了阻塞方法。
当某个方法抛出InterruptedException时,表示该方法是一个阻塞方法(比如Thread.sleep),如果这个方法被中断,那么它将努力提前结束阻塞状态。
中断是一种协作机制,一个线程在阻塞状态不能自己中断自己,它只能被其他线程中断。因此,当一个阻塞方法产生InterruptedException时,它需要决定如何应对其他线程的中断。处理方式一般就这么几种:
1、自己处理,继续流程。
2、不做异常处理继续流程,但是调用Thread.interrupt方法恢复中断标志位,使得高层知晓。
3、把异常抛出去让高层处理。
错误的处理方式是:捕获了异常,但是不做任何响应。
5.5 同步工具类
闭锁 CountDownLatch
闭锁使多个线程等待一组事件发生,当这组事件全部发生以后,所有等待的线程全部结束阻塞,继续执行。它可以用于如下场景:
所有资源都被初始化之后才继续执行。
所有依赖的服务都启动后才启动。
某个操作的参与者都就绪才开始进行。
CountDownLatch的原理和ReentrantReadWriteLock是一样的,事实上他们都是基于AbstractQueuedSynchronizer实现的。
读写锁的特性是,读写互斥、写写互斥,但是读读不互斥。所以可能出现这种场景,一堆读操作,等待一个写操作结束。这就是闭锁。
CountDownLatch构造函数接受一个int型参数,设定它需要等待的资源数,如果一个资源就绪,就调用countDown方法,将等待数减1。如果等待数不是0,那么await方法将被阻塞,无论多少个线程。当等待数到了0,这些被阻塞的线程,就会被全部释放开来。并且这把闭锁以后就失效了,无论是countDown方法还是await方法,都不再有任何控制。
这里一个典型的应用场景就是,在osgi架构中,某个操作可能需要多个osgi服务配合,但是ogsi服务又不一定及时绑定上了,就可以用闭锁阻塞。
FutureTask
FutureTask会指定一段需要被执行的代码(一个call方法,返回一个指定类型的变量),当这个call方法被执行完以后,其他线程调用FutureTask的get方法,就能得到返回值(或者接收一个异常),否则将被阻塞住。它比较适合某个资源的初始化,初始化线程和使用线程不是同一个的情况(比如osgi的bundle start里面产生FutureTask初始化资源,而业务代码get资源)。
信号量Semaphore和栅栏CyclicBarrier
前者太简单,后者太复杂,就不看了。