这本书也就将近300页的样子,关于这本书多么的厉害不多说了,相比去看这本书的人心里都有数。
个人感觉这本书还是比较偏理论,站在比较高的高度来指导你如何写出健壮的并发程序,而非介绍API的(虽然也有大量的例子)。看这种书会有种感觉就是云里雾里的,不知道自己到底吸收了多少(目前我就是这种感受),还是得靠自己有一些并发编程的经验或者多加练习,这样感觉效果会好一点。
主要是由于“重排序”的存在,在没有正确同步的多线程环境下,可能得不到数据正确的值。
加锁,不仅仅确保原子性,也可以确保可见性。 volatile变量也可以确保可见性,但是并不能保证原子性。 而且volatile变量适用于:
发布:使对象能够在当前作用域之外的代码中使用。 逸出:当某个不该发布的对象被发布时,这种情况称之为逸出。 主要是说要正确的发布对象,对象发布之后不能确保其他线程如何使用该对象,所以如果不是安全发布,那么该对象的不变性条件无法得到保证。发布一个对象的时候,可能会间接的发布该对象的非私有变量所引用的其他对象。 不要在构造过程中使this引用逸出。
满足同步需求的另外一种做法是使用不可变对象,不可变对象是绝对线程安全的。
不可变对象内部仍可以用可变对象来管理状态,注意不可变对象
和不可变的对象引用
之间的区别。
//在可变对象基础上构建的不可变对象
public final calss ThreeStooges {
private final Set<String> stooges = new HashSet<String>();
public ThreeStooges() {
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
}
public boolean inStooge(String name) {
return stooges.contains(name);
}
}
通过将域声明为final,也相应的告诉其他人员,这些域是不会变化的。 除非需要某个域是可变的,否则应将其声明为final域。
安全发布的常用模式:
在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:
感觉搞懂双重检查锁定的话,这一块的内容能了解大约一半吧。
在设计线程安全类的过程中,需要包含以下三个基本要素:
个人理解就是,找出这个对象里面可以被其他线程访问的变量,确定这些变量的不变性条件(规则),然后确保在修改这个对象的时候,时刻都能保持这些不变性条件。
基本意思就是通过加锁(私有锁对象或者是对象的内置锁)来同步对数据的访问(Java监视器模式)。
当委托失效时(比如当前类有两个变量来标示状态,且不变性条件涉及到这两个变量),需要当前类自行构建线程安全策略。
BlockingQueue: LinkedBlockingQueue
, ArrayBlockingQueue
,PriorityBlockingQueue
andSynchronousQueue
BlockingDeque: ArrayDeque
and LinkedBlockingDeque
- 可变状态是至关重要的 (所有的并发问题都可以归结为如何协调对并发状态的访问。可变状态越少,就越容易确保线程安全性。)
- 尽量将域声明为final类型,除非需要它们是可变的。
- 不可变对象一定是线程安全的 (不可变对象能极大降低并发编程的复杂性。它们更为简单且安全,可以任意共享而无须使用加锁或保护性复制等机制。)
- 封装有助于管理复杂性
- 用锁来保护每一个可变变量
- 当保护同一个不变性条件中的所有变量时,要使用同一个锁
- 在执行符合操作期间,要持有锁
- 如果从多个线程中访问同一个可变变量时没有同步机制,那么程序会出现问题
- 不要故作聪明地推断出不需要使用同步
- 在设计过程中要考虑线程安全,或者在文档中明确地指出它不是线程安全的
- 将同步策略文档化
在线程中执行任务,一要搞清楚任务边界,二要确定现场调度策略
Executor Framework - Executor (interface) - ExecutorService (newFixThreadPool, newCachedThreadPool, newSingleThreadPool, newScheduledThreadPool, 这些pool都有一个线程池,然后默认情况下使用无界的LinkedBlockingQueue来保存待执行的任务,无界LinkedBlockingQueue的大小是Integer的最大值,这通常会有一定危险,任务执行速度小于生成速度的话,很有可能导致内存溢出。(这个地方有点类似于生产者消费者)) - Future - Callable - CompletionService (Executor & BlockingQueue) - invokeAll (执行一组任务,并且按照任务集合中迭代器的顺序将所有的Future添加到返回的集合中)
取消操作的原因:
interrupt()
中断目标线程 (调用interrupt()并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息) isInterrupt()
返回目标线程的中断状态 interrupted()
清除当前线程的中断状态,并返回之前的值(始终没太搞清楚,清除中断状态和保存中断状态的意思)响应中断一般有两种策略:
可以用Future来实现取消, Future.cancel
。
处理不可中断的阻塞:
可以采用newTaskFor来封装非标准的取消 (处理不可阻塞中断这一块没遇到过,理解的一般)
shutdown()
and shutdownNow()
Thread API中提供了UncaughtExceptionHandler,设置Handler来处理未捕获异常。
守护进程 (不影响JVM的关闭)
在默认的一些线程池无法满足需求的时候,自行配置线程池(注意使用有界队列或者是无界队列)
public ThreadPoolExecutor(int corePoolSize,
int maxPoolSize,
long keepAliveTime,
TimeUnit unit,
BolckingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {...}
考虑线程池配置时候需要注意的一些:
线程工厂,很多情况下都需要使用定制的线程工厂。例如,希望为线程池中的线程制定一个UncaughtExceptionHandler,或者实例化一个定制的Thread类用于执行调试信息的记录,或者希望给线程起个有意义的名字。
扩展ThreadPoolExecutor,它提供了几个可以在子类化中改写的方法:beforeExecute、afterExecute和terminated。
死锁:
开放调用,如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用。依赖于开放调用的类,通常能表现出更好的行为。这种通过开放调用来避免死锁的方法,类似于采用封装机制来提供线程安全的方法。
class Taxi {
private Point location, destination;
private final Dispatcher dispatcher;
public synchronized Point getLocation() {
return location;
}
public void setLocation(Point location) {
boolean reachedDestination;
synchronized (this) {
this.location = location;
reachedDestination = location.equals(destination);
}
if (reachedDestination) {
dispatcher.notifyAvailable(this);
}
}
}
class Dispatcher {
private final Set<Taxi> taxis;
private final Set<Taxi> availableTaxis;
public synchronized void notifyAvailable(Taxi taxi) {
availableTaxi.add(taxi);
}
public Image getImage() {
Set<Taxi> copy;
synchronized (this) {
copy = new HashSet<Taxi>(taxis);
}
Image image = new Image();
for (Taxi t : copy) {
image.drawMarker(t.getLocation());
}
return image;
}
}
通过可定时的锁可以避免死锁,通过线程转储信息可以分析死锁。
其他活跃性危险
最常见的活跃性问题就是锁顺序死锁,在设计程序时应该避免产生锁顺序死锁:确保线程在获取多个所时采用一致的顺序,最好的解决办法是在 程序中始终使用开放性调用 。
衡量程序的性能一般可以从这两个方面:运行速度和处理能力
在进行程序优化的时候(不管是“多块”还是“多少”),首先使程序正确,然后再提升性能,提升性能的时候要搞清楚想要提升的是哪方面的指标,而且要以测试为基准,不要妄加猜测。
关于多快,最终还是要受限于任务中有多少串行的部分,最高加速比为 1/ (F + (1-F)/N), 当N(处理器个数)趋近于无限大的时候,加速比趋近于1/F(串行部分的比例)。
所以理想情况下,所有可以并行的地方都并行执行且相对于串行部分,时间可以忽略不计,那么任务的运行时间就约等于任务中串行部分需要的时间。
关于多少,基本意思就是在增加计算资源(CPU,内存,存储容量,带宽)的时候,程序的吞吐量或者处理能力能够得到相应地增加。就是可扩展性。
线程引入的开销:
减少锁的竞争,在并发程序中,对可伸缩性的最主要的威胁就是独占方式的资源锁:
并发测试大致分为两类:安全性测试(不发生任何错误行为)和活跃性测试(某个良好的行为终究会发生)
这部分主要还是需要结合书中提供的例子
ReentrantLock实现了Lock接口,并提供了与synchronized相同的互斥行和内存可见性。 ReentrantLock提供了更加灵活的功能:定时锁(tryLock()),轮询锁(tryLock()),可中断的锁获取操作(tryLock(timeout, unit)),非块结构的加锁,提供公平锁;而且性能比内置锁更好。
与显式锁相比,内置锁有自己的优势,内置锁为开发人员所熟悉,并且简介紧凑,现有许多程序都已经使用了内置锁;显式锁更危险,因为需要明确的unlock,内置锁自动释放锁;内置锁在线程转储中能给出哪些调用帧获得了哪些锁;内置锁是JVM的内置属性(synchronized),它能执行一些优化(粗化锁粒度,单线程访问消除锁之类的),而且未来更有可能提升内置锁的性能。
读写锁的一些可选实现包括:
这些可选实现是在自己实现ReadWriteLock的时候可以选择实现的功能,ReentrantReadWriteLock为这两种锁都提供了可重入的加锁语义,而且在构造的时候可以选择是一个公平的锁还是非公平的。
状态依赖性,条件队列,条件谓词 Lock -- 内置锁, Condition -- 内置条件队列 AbstractQueuedSynchronizer
这一章节前面都是一些理论,消化的一般;后面在介绍AQS,concurrent包里面很多同步工具都是基于AQS构建的。
CAS -- compare and swap
在竞争程度较高的情况下(线程本地计算较少),锁的性能要比原子变量好;但是在更真实的情况下,原子变量的性能将超过锁(竞争程度适中)。 另外,在这两种情况下,ThreadLocal的性能都是最好的。
不太明白为什么这里竞争程度适中的情况会更加真实,大概是因为书中来计较的时候,竞争程度较高的这种情况下,除了竞争锁或原子变量,什么都不做。
如果在某种算法中,一个线程的失败或者挂起不会导致其他线程也失败或者挂起,那么这种算法就被称为非阻塞算法。 如果在算法的每个步骤中都存在某个线程能够执行下去,那么这种算法被称为无锁算法。 如果在算法中仅将CAS用于协调线程之间的操作,并且能正确地实现,那么它既是一种无阻塞算法,也是一种无锁算法。
private static class Node <E> {
public final E item;
public Node<E> next;
public Node(E item) {
this.item = item;
}
}
private static class NodeQ <E> {
public final E item;
public AtomicReference<NodeQ<E>> next;
public NodeQ(E item, NodeQ<E> next) {
this.item = item;
this.next = new AtomicReference<NodeQ<E>>(next);
}
}
//非阻塞栈
public class ConcurrentStack <E> {
AtomicReference<Node<E>> top = new AtomicReference<Test.Node<E>>();
public void push(E item) {
Node<E> newHead = new Node<E>(item);
Node<E> oldHead;
do {
oldHead = top.get();
newHead.next = oldHead;
} while (!top.compareAndSet(oldHead, newHead));
}
public E pop() {
Node<E> oldHead;
Node<E> newHead;
do {
oldHead = top.get();
if (null == oldHead) {
return null;
}
newHead = oldHead.next;
} while (!top.compareAndSet(oldHead, newHead));
return oldHead.item;
}
}
//非阻塞链表
public class LinkedQueue <E> {
private final NodeQ<E> dummy = new NodeQ<E>(null, null);
private final AtomicReference<NodeQ<E>> head = new AtomicReference<NodeQ<E>>(dummy);
private final AtomicReference<NodeQ<E>> tail = new AtomicReference<NodeQ<E>>(dummy);
public boolean put(E item) {
NodeQ<E> newNodeQ = new NodeQ<E>(item, null);
while (true) {
NodeQ<E> currentTail = tail.get();
NodeQ<E> tailNext = currentTail.next.get();
if (currentTail == tail.get()) {
if (tailNext != null) {
//队列处于中间状态,推进尾节点
tail.compareAndSet(currentTail, tailNext);
} else {
//队列处于稳定状态,尝试插入新节点
if (currentTail.next.compareAndSet(null, newNodeQ)) {
//插入操作成功,尝试推进尾节点;此时如果推进尾节点不成功,可由其他线程帮其推进。
tail.compareAndSet(currentTail, newNodeQ);
return true;
}
}
}
}
}
}
CAS中的ABA问题,AtomicStampedReference(添加版本号)和AtomciMarkableReference(将节点保留在队列中的同时,将其标记为“已删除的节点”)通过不同的手段可以避免ABA问题。
参阅ifeve.com