读《Java并发编程实践》
随着多核处理器的普及,使用并发成为构建高性能应用程序的关键。
多处理器与多核处理器的区别:多核是指一个处理器里面有多个处理核心,而多处理器就是在一台机器上有多个处理器。很多商家在卖机的时候仍将一个处理核心说成是一个处理器,将多核说成多处理器,一般单机上多数人说的多处理器就是多核的意思 。如下图示,
多处理器系统
多核处理器
过去的三十年间,计算机性能一直由摩尔定律来推动;从今天起,它将由Amdahl(阿姆德尔)定律推动。
Java 5.0 是在Java开发并发应用程序的进程中,迈出的巨大一步。它提供了新的高层组件以及更多的低层机制,这些将使得一名新手更容易像专家那样去构建并发应用程序。
线程是控制和利用多处理器系统计算能力的最简单的方式。同时,伴随着处理器数量的增加,有效地采用并发会变得越来越重要。
因为程序调度的基本单元是线程,一个单线程应用程序一次只能运行在一个处理器上。使用多线程可以帮助我们在单处理器系统中实现更佳的吞吐量。例如,当第一个线程等待I/O而阻塞时,另一个线程也可以运行,这样就可以提高CPU的利用效率。
下面是读《Java并发编程实践》后的总结
曾经对Java并发编程不屑一顾,曾经也听到过别人对Java多线程不屑一顾,而今真正看了《Java并发编程实践》这本书才让我懂得以前的我是多么的渺小,犹如沧海一粟;又如井底之蛙,坐井观天。我们知道,使用多线程最主要的原因就是提高性能,然而伴随着性能的提高,程序的安全性浮现在了我们的面前。故,如何提高性能的同时保证程序的安全性是我们研究核心所在。再次提醒,安全性永远排在第一位,它比提高性能更重要!
第一章,简介。简短地描述多线程产生、发展的因素;使用线程的一些优点;伴随着线程带来的安全风险;最后,我们须知:线程无处不在。
第二章,线程安全。什么样的类是线程安全的?原子性的概念;锁的介绍;如何用锁来保护状态;活跃度及性能的引入。
1, 保证线程安全的三种方法 :
a, 不要跨线程访问共享变量;
b, 使共享变量是 final类型的;
c, 将共享变量的操作加上同步。
2, 一开始就将类设计成线程安全的,比在后期重新修复它,更容易。
3, 编写多线程程序, 首先保证它是正确的,其次再考虑性能。
4, 无状态或只读对象永远是线程安全的。
5, 不要将一个共享变量裸露在多线程环境下(无同步或不可变性保护)。
6, 多线程环境下的延迟加载需要同步的保护, 因为延迟加载会造成对象重复实例化。
7, 对于 volatile 声明的数值类型变量进行运算, 往往是不安全的 (volatile 只能保证可见性, 不能保证原子性)。
8, 当一个线程请求获得它自己占有的锁时 (同一把锁的嵌套使用),我们称该锁为可重入锁。在 jdk1.5 并发包中, 提供了可重入锁的 java 实现 -ReentrantLock.
9, 每个共享变量,都应该由一个唯一确定的锁保护。创建与变量相同数目的 ReentrantLock, 使他们负责每个变量的线程安全。
10,虽然缩小同步块的范围,可以提升系统性能。但在保证原子性的情况下,不可将原子操作分解成多个synchronized块。
第三章,共享对象。什么是可见性?发布和逸出。
11, 在没有同步的情况下,编译器与处理器运行时的指令执行顺序可能完全出乎意料。原因是,编译器或处理器为了优化自身执行效率,而对指令进行了的重排序 (reordering)。
12, 当一个线程在没有同步的情况下读取变量, 它可能会得到一个过期值, 但是至少它可以看到那个线程在当时设定的一个真实数值,而不是凭空而来的值,这种安全保证,称之为最低限的安全性 (out-of-thin-air safety)。
13, volatile 变量, 只能保证可见性,无法保证原子性。
14, 某些耗时较长的网络操作或IO,确保执行时不要占有锁。
15, 发布 (publish) 对象,指的是使它能够被当前范围之外的代码所使用(引用传递)。
对象逸出 (escape), 指的是一个对象在尚未准备好时将它发布。
原则:为防止逸出,对象必须要被完全构造完后,才可以被发布 ( 最好的解决方式是采用同步 )。
this 关键字引用对象逸出
例子:在构造函数中,开启线程,并将自身对象 this 传入线程,造成引用传递。而此时,构造函数尚未执行完,就会发生对象逸出了。所以,应避免在构造函数中启动线程!
16, 必要时,使用 ThreadLocal变量确保线程封闭性 (封闭线程往往是比较安全的,但一定程度上会造成性能损耗 )。
封闭对象的例子在实际使用过程中比较常见,例如 hibernate openSessionInView机制,jdbc的 connection机制 .
17, 单一不可变对象往往是线程安全的 (复杂不可变对象需要保证其内部成员变量也是不可变的 )。
良好的多线程编程习惯是:将所有的域都声明为 final,除非它们是可变的 。
18, 保证共享变量的发布是安全的 。
a, 通过静态初始化器初始化对象 (jvm 会保证静态初始化变量是同步的 );
b, 将对象申明为 volatile 或使用 AtomicReference;
c, 保证对象是不可变的;
d, 将引用或可变操作都由锁来保护。
19, 设计线程安全的类,应该包括的基本要素:
a, 确定哪些是可变共享变量;
b, 确定哪些是不可变的变量;
c, 指定一个管理并发访问对象状态的策略。
20, 将数据封装在对象内部,并保证对数据的访问是原子的。建议采用 volatile javabean 模型或者构造同步的 getter,setter.
21, 线程限制性使构造线程安全的类变得更容易,因为类的状态被限制后,分析它的线程安全性时,就不必检查完整的程序。
22, 编写并发程序,需要更全的注释,更完整的文档说明。
23, 在需要细分锁的分配时,使用 java监视器模式好于使用自身对象的监视器锁。前者的灵活性更好。
Object target = new Object();
// 这里使用外部对象来作为监视器, 而非 this
synchronized(target) {
// TODO
}
第四章,组合对象。
24, 设计并发程序时, 在保证伸缩性与性能折中的前提下,优先考虑将共享变量委托给线程安全的类,由它来控制全局的并发访问。
第五章,构建块。同步容器的介绍;并发容器的添加;以及一些Synchronizer的概念,比如Latch、FutureTask、Semaphore及Barriers等。
25, 使用普通同步容器 (Vector, Hashtable) 的迭代器,需要外部锁来保证其原子性 。原因是 普通同步容器产生的迭代器是非线程安全的。
26, 在并发编程中,需要容器支持的时候,优先考虑使用 jdk 并发容器 (ConcurrentHashMap, ConcurrentLinkedQueue, CopyOnWriteArrayList…)
27, ConcurrentHashMap, CopyOnWriteArrayList并发容器的迭代器,以及全范围的 size(), isEmpty() 都表现出弱一致性。他们只能标示容器当时的一个数据状态 ,无法完整响应容器之后的变化和修改。
28, 使用有界队列,在队列充满或为空时,阻塞所有的读与写操作 ( 实现生产者 – 消费者的良好方案 )BlockQueue 下的实现有 LinkedBlockingQueue 与 ArrayBlockingQueue, 前者为链表,可变操作频繁优先考虑;后者为数组,读取操作频繁优先考虑。
PriorityBlockingQueue 是一个按优先级顺序排列的阻塞队列,它可以对所有置入的元素进行排序 ( 实现 Comparator 接口 )。
29, 当一个方法能抛出 InterruptedException,则意味着这个方法是一个可阻塞的方法。如果它被中断,将提前结束阻塞状态。当你调用一个阻塞方法,也就意味着本身也称为了一个阻塞方法,因为你必须等待阻塞方法返回。如果阻塞方法抛出了中断异常,我们需要做的是,将其往上层抛,除非当前已经是需要捕获异常的层次。如果当前方法不能抛出 InterruptedException,可以使用 Thread.currentThread.interrupt() 方法,手动进行中断。
第六章,任务执行。如何在线程中执行任务?Executor框架的介绍;Callable及Future的认识。
第七章,取消和关闭。
第八章,应用线程池。
第九章,GUI应用程序。
第十章,避免活跃度危险。介绍了死锁的概念;如何诊断和避免死锁;其它的活跃度危险:饥饿、弱响应性及活锁。
第十一章,性能和可伸缩性。对性能的全方位剖析,Amdahl定律的引入;线程引入的开销;如何减少锁的竞争。
第十二章,测试并发程序。
第十三章,显示锁。Lock和ReentrantLock;对性能的考量;公平性;在synchronized和ReentrantLock之间进行抉择;读写锁。
第十四章,构建自定义的同步工具。
第十五章,原子变量与非阻塞同步机制。锁的劣势;CAS的概念;原子变量类Atomic;非阻塞算法。
第十六章,Java存储模型。什么是存储模型,要它何用?如何安全发布。
实话实说,已经看了两遍《java并发编程实践》了,但差不多现在只掌握了30%的内容。革命尚未成功,同志仍需努力!继续感悟。。。