1.同步容器,同步容器包括Vector和HashTable,是早期jdk的一部分。另一部分是同步包装类,以Collections.synchronizedxxx的工厂方法创建。
2.同步容器虽然是线程安全的,但是对于复合操作,有时你可能需要加上额外的客户端加锁进行保护,即对于使用这些容器的客户端代码,如果存在复合操作,还是可能存在风险。
3.例如check-and-act操作、循环中的元素操作等,如果在客户端代码中没有额外的锁,都会发生意想不到的问题。
4.造成这些的问题都可以通过在客户端加锁来解决,但是这完全阻止了其他线程在加锁期间对整个容器的修改,降低了并发性 。
5.以Vector容器为例,如果在迭代过程当中发现容器被修改,就会抛出ConcurrentModificationException。
6.容器的toString、equals等方法都有可能会间接得调用迭代器,从而导致ConcurrentModificationException。
7.java5通过几种并发容器解决容器并发的问题:同步容器通过对所有的状态进行串行访问,从而实现了他们的线程安全,这样做的坏处是当多个线程共同竞争容器级锁的时候,吞吐量会降低
8.HashMap对应的并发实现时ConcurrentHashMap,当多数操作为读的时候,CopyOnWriteArrayList是list的并发实现,这些接口提供了对一些复合操作的支持,所以不需要客户端代码增加额外的并发也能正确执行。
9.java5中同样增加了Queue和BlockQueue,jdk提供了几种实现:传统的FIFO队列ConcurrentLinkedQueue、具有优先级的PriorityQueue,这些queue并不会阻塞。
10.java5同样提供了BlockQueue,它增加了可阻塞的插入和获取操作,如果队列是空的,获取操作会被阻塞,对于有界队列,如果队列已满,则插入曹祖偶会被阻塞,这一特性对于实现生产者消费者类型的程序非常有用。
11.同步容器在每个操作的时候都持有一个锁。对于容器的一些操作:例如HashMap的get,List的contains操作等可能需要遍历大量数据的操作,并发下的效率可能会降低。以HashMap的get操作为例,如果Map中的key经过hash后没有被散列均匀,极端得被放置在同一个链表当中,get操作就需要遍历整个链表并调用他们的equals操作。在并发下进行这个操作,其他线程就不能操作容器中的元素,而且由于效率较低,等待的时间也会很长,最终导致并发下的低效率。
12.ConcurrentHashMap使用了分离锁的机制来提高HashMap在高并发下访问的效率问题。ConcurrentHashMap提供了不会抛出ConcurrentModificationException的迭代器,这种迭代器提供弱一致性的支持。此外,那些对整个容器进行操作的方法(size、isEmpty)被弱化了,转而保证容器最基础的get、put、remove、containsKey等操作。ConcurrentHashMap还提供了一些标准的复合操作接口,例如缺少即加入、相等便移除、相等便替换等。
13.相比HashTable和synchronizedMap,应该使用ConcurrentHashMap。14.CopyOnWriteArrayList是ArrayList的改进实现,CopyOnWriteArrayList在内部维护一个不可变数组(长度、数组元素不可变,数组本身引用可变),对于所有的读操作,读取这个不可变数组不会出现并发异常,例如并发下迭代CopyOnWriteArrayList,如果同时有线程对该容器进行了修改,不会抛出ConcurrentModificationException,因为迭代器创建的时候就决定了只能使用哪个时刻的数据备份,正因为这个原因,他也不能及时接受到新的数据修改。对于写操作,CopyOnWriteArrayList的做法是复制一个数组,在新的数组上进行修改,然后set回CopyOnWriteArrayList中维护的基础数据引用中。这样导致在写的时候性能的下降。
15.阻塞队列的两个方法put和take是阻塞方式的,如果队列满了执行put,动作会阻塞,如果take时队列是空的,动作也会阻塞。阻塞队列同时提供了offer方法,如果offer动作无法把数据放入队列,会返回一个失败状态。
16.LinkedBlockingQueue和ArrayBlockQueue是FIFO队列,PriorityBlockingQueue是可以按照优先级顺序排序的队列。BlockingQueue的另外一种实现是SynchronousQueue, SynchronousQueue中不存储数据,他只能允许有线程在等待的时候才能put进去数据,当没有线程在消费数据时,put操作会被阻塞。
16.Synchronizer封闭状态,状态决定着线程执行到某一点时是通过还是被迫等待;Synchronizer还提供操控状态的方法,以及高效等待Synchronizer进入期望状态的方法,即在Synchronizer的api级别用提示性强的api提供给用户操作和等待状态的方法,不需要使用类似同步快这种可读性不强的操作(例如阻塞队列的take、put,CountDownLatch的countDown和await等方法);
17.闭锁Latch是一种Synchronizer,他可以延迟线程的状态,直到闭锁达到终止状态。当闭锁达到终止状态前,所有等待闭锁的线程都在等待,而当闭锁达到终止状态后,所有线程都可以执行了。典型的利用场景是多个线程都依赖某个基础数据的初始化,那就可以在初始化时使用闭锁,其他用到该数据的地方等待闭锁,直到初始化完后,释放闭锁,等待的线程就可以继续执行了。
18.FutureTask同样是一种闭锁,他标识一个可以等待返回值的Callable线程。他的get方法的调用会等待线程执行完成或者达到取消状态,你可以在需要获取结果的线程中线执行FutureTask的任务,在需要数据时再从FutureTask对象中调用get方法获取该数据,如果这个数据获取是一个耗时的计算,则可以提供需要数据线程的响应时间。
19.信号量Semaphore用来控制能够同时访问某特定资源的活动的数量,Semaphore管理一个有效的许可集,活动能够获得许可,完成后释放许可。信号量可以用来实现资源池,把信号量的许可数量维护成容器的size,获得一个容器的项减掉一个信号量许可,当容器中为空时,即信号量为0,你要做的事阻塞容器的获取操作,直到容器不为空为止,容器增加一个容器值,信号量随着增加1。使用Semaphore,你可以把任何容器转换成有界的阻塞队列。
20.关卡类似于闭锁,他们都可以阻塞一组线程,等待一个时间的发生,闭锁是多个线程等待一个事件发生,而关卡则是用于每个线程都在等所有线程共同达到一个状态的场景。CyclicBarrier允许你像构造函数传递关卡行为,当成功通过关卡时会执行,但是在阻塞被释放之前是不执行的。关卡通常用于一个这样的场景:一个计算可以并行完成,但是必须完成于一个步骤相关的操作之后才能进入下一步。例如:每个省的税收计算任务都很复杂,我们可以给每个省开一个线程开始计算,然后为这些计算设置一个CyclicBarrier,当每个线程都完成的时候,再进行汇总计算的工作。
21.如何为计算结果建立高速的缓冲?
21.1使用HashMap存储计算结果,每次请求检查缓冲中使用有值,如没有则重新计算,如有则直接返回,但问题是HashMap不是线程安全的,在检查结果和put结果之间存在check-and-act操作,所以容易导致重复计算的问题,需要把整个check-and-act操作都封装到同步块当中。
22.2HashMap的改进版本是使用ConcurrentHashMap,但是同样还是存在check-and-act操作,如果缓存中没有值,会尝试去计算出值再放入缓存,计算的过程比较久的情况下,会导致可能有很多的线程都进行重复的计算,存在比较严重的资源浪费情况。
22.3在上一版本的基础上,可以引入FutureTask这个异步操作的方式来初始化缓存,即当缓存中没有值时,先放入一个FutureTask封装计算的过程,这个封装的步骤肯定是不耗时的,因为真正的计算可以异步执行,然后往缓存中放入FutureTask对象,让后续的请求从FutureTask中获取缓存内容。
22.4但是即使用了上一版本的方法,依然可能在高并发下存在多个线程同时初始化了FutureTask对象的情况,为了避免两个FutureTask同时工作,可以使用ConcurrentHashMap的putIfAbcent方法,达到只有一个FutureTask放入缓存成功的目的,放入成功后再开始异步计算。通过这种方式可以达到高效维护缓存的目的.
java 并发编程读书笔记