读书笔记:《分布式JAVA应用 基础与实践》 第四章 分布式JAVA应用与JDK类库
线程安全的HashMap实现,主要原理是将集合分成多个段(默认16个),分段加锁,实现高并发。
据作者的测试结果,在线程数较少时,无论元素数量多少,ConcurrentHashMap带来的性能提升不明显,但在线程数为50以上时,ConcurrentHashMap在增加和删除时性能提升了一倍左右,查找时性能提升了10倍左右。
因而,在并发场景,ConcurrentHashMap较之HashMap是更好的选择
据作者的测试结果,随着元素数量和线程数量增加,CopyOnWriteArrayList在增加元素和删除元素时性能下降明显,比ArrayList更低。但在查找元素上却比ArrayList好很多(大于10倍)。
因此,在读多写少的并发场景,CopyOnWriteArrayList较之ArrayList是更好的选择。
基于CopyOnWriteArrayList实现,唯一不同的是每次add时调用的是addIfAbsent方法,此方法需遍历当前Object数组,检查待增加的元素是否已存在,已存在则直接返回。故性能略低于CopyOnWriteArrayList
一个基于固定大小数组、ReentrantLock以及Condition实现的可阻塞的先进先出的Queue.
类似的还有过LinkedBlockingQueue, 对put和offer采用一把锁,对take和poll采用另一把锁,避免了读写时互相竞争锁的现象,因此,在高并发读写操作都多的情况下,性能会较ArrayBlockingQueue好很多
一个支持原子操作的Integer类,在没有AtomicInteger前,要实现一个按顺序获取的ID,就必须在每次获取时进行加锁操作,以避免出现并发时获取到同样ID的现象。借助AtomicInteger,则可以更简易地实现,如下:
Private staticAtomicInteger counter = new AtomicInteger();
Public staticint getNextId() {
?????? Return counter.incrementAndGet();
}
incrementAndGet方法基于CPU的CAS原语实现,基于CAS的操作可以认为是无阻塞的,因此性能好于同步锁的方式。
对于使用JDK5以上的版本时,应尽量使用atomic的类,除AtomicInteger外,还有AtomicLong、AtomicBoolean、AtomicReference等。
一个高效的、支持并发的线程池,要想用好这个线程池,需要合理配置corePoolSize,最大线程数、任务缓冲的队列,以及线程池满时的处理策略。常见的需要有如下两种
如果希望高性能地执行Runnable任务,即当线程池中的线程数尚未达到最大个数,则立刻交给线程执行或在最大线程数量的保护下创建线程来执行,可选的方式为使用SynchronousQueue作为任务的队列。
如果希望Runnable任务尽量被corePoolSize范围的线程执行掉,可选的方式为使用ArrayBlockingQueue或LinkedBlockQueue作为任务缓冲的队列。这样,当线程数等于或超过corePoolSize后,会先加入缓冲队列中,而不是直接交由线程执行。
创建固定大小的线程池keepAliveTime为0,即线程启动后就不退出,缓冲队列为LinkedBlockingQueue,大小为整型的最大数
创建大小为1的固定线程池
创建corePoolSize为0最大线程数为整型的最大数,线程keepAliveTime为1分钟,缓存任务的队列为SynchronousQueue的线程池
创建corePoolSize为传入参数,最大线程数为整型的最大数,线程keepAliveTime为0, 缓存任务的队列为DelayedWorkQueue的线程池。在实际业务中,通常会有一些需要定时或延迟执行的任务,更典型的是在异步操作时需要超时回调的场景。
JDK5之前,用Timer实现这个功能,两者区别:
可用于异步获取执行结果或取消执行任务的场景,通过传入Runnable或Callable的任务给FutureTask,直接调用其run方法或放入线程池执行,之后可在外部通过FutureTask的get异步获取执行结果。FutureTask可以确保即使调用了多闪run方法,它都会只执行一次Runnable或Callable任务。
用于控制某资源同时被访问的个数的类,例如连接池中控制创建的连接个数。
?
可用于控制多个线程同时开始某动作的类,采用的方式为减计数方式。当计数减至零时,位于latch.await后的代码才会被执行。
和CountDownLatch不同,CyclicBarrier是当await数量达到设定的数量后,才继续向下执行。
一个更为方便的控制并发资源的类,和synchronized语法达到的效果是一致的。这两者之间如何取舍呢,借用Brian Goetz的话(http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html):
答案非常简单 —— 在确实需要一些 synchronized 所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票。 ReentrantLock 还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是请记住,大多数 synchronized 块几乎从来没有出现过争用,所以可以把高度争用放在一边。我建议用 synchronized 开发,直到确实证明 synchronized 不合适,而不要仅仅是假设如果使用ReentrantLock “性能会更好”。请记住,这些是供高级用户使用的高级工具。(而且,真正的高级用户喜欢选择能够找到的最简单工具,直到他们认为简单的工具不适用为止。)。一如既往,首先要把事情做好,然后再考虑是不是有必要做得更快。
接口,典型实现ReentrantLock,ReentrantLock提供了一个newCondition方法,以便用户在同一个锁的情况下可以根据不同的情况执行等待或唤醒动作。
ReentrantLock.newCondition().await()
ReentrantLock..newCondition().signal()
和ReentrantLock没有继承关系,它提供了读锁(ReadLock)和写锁(WriteLock),比ReentrantLock的一把锁,读写锁分离对读多写少的情况,提高性能。
当调用读锁的lock方法时,没有线程持有写锁,就可获得读锁,意味着只要进行读操作,就没有其它线程进行写操作,读操作时无阻塞的;
当调用写锁的lock时,如果此时没有线程持有读锁或写锁,则可继续,意味着要进行写时,如果有其它线程进行读或写,就会被阻塞,
读写锁使用时的升级和降级机制:
同一线程中,持有读锁后,不能直接调用写锁的lock
同一线程中,持有写锁后,可调用读锁的lock方法,之后如果调用写锁的unlock方法,那么当前锁将降级为读锁。
?
记作者的测试: 同时启动102个线程,100个进行读操作(HashMap.get),2个进行写操作(HashMap.put), 较之ReentrantLock,ReentrantReadWriteLock性能提升了三倍多。
?
以上分析了JDK5以后版本提供的并发包中一些常用类的实现方法,这些类能够帮助开发者更好地控制高并发下的资源操作,尽可能地避免出现不一致及资源竞争激烈等现象。总的来说,基于CAS、拆分锁、voliate及AbstractueuedSynchronier是这些类的主要实现方法,这些方法都是为了尽量减少高并发时的竞争现象,对于实际编码有一定的参考价值,从而保障程序即使在高并发时也能保持较高的性能。
?
?
?
?
?
?