1. 如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题:
    不在线程之间共享该状态变量
    将状态变量修改为不可变的变量
    在访问状态变量时使用同步

  2. 当设计线程安全的类时,良好的面向对象技术、不可修改性,以及明晰的不变性规范都能起到一定的帮助作用。

  3. 当多个线程访问某个类时,不管运行时环境采用何种调度方式或这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同, 这个类都能表现出正确的行为,那么就称这个类时线程安全的。

  4. 无状态对象一定是线程安全的,无状态对象:既不包含任何域也不包含任何对其他类中域的引用。

  5. 假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。原子操作是指,对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作。

  6. 在实际情况中,应尽可能地使用现有的线程安全对象(例如 AtomicLong)来管理类的状态。与非线程安全的对象相比,判断线程安全对象的可能状态及其状态转换情况要更为容易,从而也更容易维护和验证线程安全性。

  7. 要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。

  8. 对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。

  9. 当时执行时间较长的计算或者可能无法快速完成的操作时(例如,网络IO或控制台IO),一定不要持有锁。

  10. 在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺寻进行判断,几乎无法得到正确的结论。

  11. 非volatile类型的64位数值变量(double和long)在多线程共享时是不安全的,因为JVM允许将64为的读操作和写操作分解为两个32位的操作,当对于一个64位的变量,如果读操作和写操作分别在不同的线程中执行,那么很可能读到某个值的高32位和另一个值的低32位。在多线程环境下需要用volatile 或者锁保护起来。

  12. 加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。

  13. 访问volatile变量时不会执行加锁操作,因此不会执行线程阻塞,所以volatile变量是一种比sunchronized关键字更轻量级的同步机制。

  14. 仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。volatile变量的正确使用方式包括:确保它们自身状态的可见性,确保它们所引用对象的状态的可见性,以及标识一些重要的程序生命周期事件的发生(例如,初始化或关闭)。volatile通常用于某个操作完成、发生中断或者状态的标识。volatile不足以保证递增(count++)操作的原子性,枷锁机制即可以保证可见性又可以保证原子性,而volatile变量只能保证可见性。

  15. 对于服务器应用程序,无论在开发阶段还是测试阶段,当启动JVM时一定要指定-server命令行选项。server模式的JVM将比client模式的JVM进行更多的优化,例如将循环中未被修改的变量提升至循环外部。

  16. 当且仅当满足一下所有条件时,才应该使用volatile变量:对变量的写入操作不依赖于变量的当前值,或者你能确保只有单个线程更新变量的值。该变量不会与其他状态变量一起纳入不变性条件中。在访问变量时不需要枷锁。

  17. 锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。Volatile变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。因此,单独使用 volatile 还不足以实现计数器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。

  18. Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值,Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。

  19. 不要在构造过程中将this引用逸出,即使是在构造函数的最后一句。在构造期间不要公布this引用,不要隐式地暴露this引用,不要在构造函数内启用线程。确定 this 引用对其它线程是否可见是一项很棘手的工作。最好的策略是,在构造函数中完全避免使用 this 引用(直接或间接)。然而,实际上这种完全避免使用 this 引用的可能并不总是存在。但只要记住,在 构造函数中谨慎使用 this 引用和创建非静态内部类的实例。

  20. 维持线程封闭性的一种更规范的方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set等方法,这些方法为每个使用该变量的线程都存有一份独立的副本。

  21. 不可变对象一定是线程安全的。满足以下条件时,对象才是不可变的:

    1. 对象创建后其状态就不能修改
    2. 对象的所有域都是final类型
    3. 对象是正确创建的(在对象创建期间,this引用没有逸出)
  22. 任何线程都可以在不需要额外同步的情况下安全的访问不可变对象,即使在发布这个对象时没有使用同步。java内存模型为不可变对象的共享提供了一种特殊的初始化安全保证。然而如果final域所指向的是可变对象,那么在访问这些域时仍然需要同步。

  23. 要安全的发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确的构造对象可以通过以下方式来安全的发布:

    1. 在静态初始化函数中初始化一个对象引用 public static Object o = new Object();
    2. 将对象的引用保存到volatile类型的域或者AtomicReferance对象中
    3. 将对象的引用保存到某个正确构造对象的final域中
    4. 将对象的引用保存到一个由锁保护的域中
  24. 对象的发布需求取决于它的可变性:
    1. 不可变对象可以通过任意机制来发布
    2. 事实可变对象必须通过安全方式来发布
    3. 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来
  25. 在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:
    1. 线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
    2. 只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
    3. 线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。
    4. 保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。
  26. 在设计线程安全类的过程中,需要包含以下三个基本要素:
    1. 找出构成对象状态的所有变量,如果对象的所有域都是基本类型,那么这些域就构成了对象的全部状态,如果对象中引用了其他对象那么这个对象的状态包括被引用的对象的域。
    2. 找出约束状态变量的不变性条件
    3. 建立对象状态的并发访问管理策略
  27. 如果不了解对象的不变性条件与后验条件,那么就不能确保线程安全性。要满足在状态变量的有效值或状态转换上的各种约束条件,就需要借助于原子性与封装性

  28. 将数据封装在对象的内部,可以将数据的访问权限限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。

  29. 如果一个类是有多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性委托给底层里的状态变量。

  30. 如果一个变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全的发布这个变量。

  31. 正如封装对象的状态有助于维持不变性条件一样,封装对的同步机制同样有助于确保实施同步策略。容器的toString、hashCode、equals、containsAll、removeAll、retainAll等方法,以及吧容器作为参数的构造函数,都会对容器进行迭代,都有可能抛出ConcurrentModificationnException。

  32. 通过并发容器来代替同步容器,可以极大地提高伸缩性并降低风险。并发容器的迭代器不会抛出ConcurrentModificationnException。

  33. ConcurrentHashMap并不是将每个方法都在同一个锁上同步并使每次只能有一个线程访问容器,而是使用一种粒度更细的加锁机制来实现更大程度的共享,这种机制称为分段锁,在此机制中,任意数量的读取线程可以并发的访问Map,执行读取操作的线程和执行写入操作的线程可以并发的访问Map,并且一定数量的写入线程可以并发的修改Map。

  34. 在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具:他们能抑制并防止产生过多的工作项,使应用程序在负荷过载的情况下变得更加健壮。

  35. CountDownLatch是一种灵活的闭锁实现,它可以使一个或多个线程等待一组事件发生。闭锁状态包括一个计数器,该技术器初始化为一个正数,表示需要等待的事件数量。countDown方法递减计数器,表示有一个时间已经发生了,而await方法等待计数器为零,表示所有等待的时间都已经发生。如果计数器非零,那么await方法会一直阻塞直到计数器为零或者等待中的线程中断,或者等待超时。这种现象只出现一次——计数无法被重置

  36. 闭锁是一种同步工具类,可以延迟线程的进度直到其到达终止状态。

  37. FutureTask也可以用作闭锁,其实现了future接口,表示一种抽象的可生成结果的计算。FutureTask表示的计算是通过Callable来实现的,相当于一种可生成结果的Runnable。

  38. 信号量(Semaphore)来控制同时访问某个特定资源的操作数量,或者同时执行某个制定操作的数量。如果没有许可,acquire将阻塞直到有许可。release方法将返回一个许可给信号量。其简化形式是二值信号量,可以用作互斥体,具备不可冲入的加锁语义:谁拥有这个唯一的许可,谁就拥有了互斥锁。

  39. 栅栏(Barrier)类似于闭锁,它能阻塞一组线程直到某个事件发生。栅栏与闭锁的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行。

  40. 第一部分总结:

    1. 可变状态是至关重要的。所有的并发问题都可以归结为如何协调对并发状态的访问,可变状态越少,就越容易确保线程安全性
    2. 尽量将域声明为final类型,除非需要他们是可变的。
    3. 不可变对象一定是线程安全的。不可变对象能极大地降低并发编程的复杂性。他们更为简单而安全,可以任意共享而无须使用加锁或保护性复制等机制
    4. 封装有助于管理复杂性。在编写线程安全的程序时,虽然可以将所有数据都保存在全局变量中,但将数据封装在对象中,更易于维持不变性条件:将同步机制封装在对象中,更易于遵循同步策略
    5. 用锁来保护每个可变变量。
    6. 当保护同一个不变形条件中的所有变量时,要使用同一个锁。
    7. 在执行复合操作期间,要持有锁。
    8. 如果从多个线程中访问同一个可变变量时没有同步机制,那么程序会出现问题。
    9. 不要故作聪明的推断出不需要使用同步
    10. 在设计过程中考虑线程安全,或者在文档中明确的支出它是不是线程安全的
    11. 将同步策略文档化
  41. 每当看到下面这种形式的代码时:new Thread(runnable).start();并且你希望获得一种更灵活的执行策略时,请考虑Executor来代替Thread。

  42. Timer负责管理延时任务和周期任务,其缺陷:

Timer在执行定时任务时只会创建一个线程,如果某个任务的执行时间过长,那么将破坏其他TimerTask的定时精确性。
如果TimerTask抛出了未检查的异常,那么Timer将表现出糟糕的行为。Timer线程不捕获异常,因此当TimerTask抛出异常时将终止定时线程。

  1. Future表示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或取消,以及获取任务的结果和取消任务等。其隐含意义是,任务的生命周期只能前进,不能后退,一个任务完成后,它就永远停留在完成完成状态。

  2. 只有当大量相互独立且同构的任务可以并发进行处理时,才能体现出将程序的工作负载分配到多个任务中带来的真正性能提升。

  3. CompletionService是将产生新的异步任务与使用已完成任务结果分离开来的服务。生产者submit执行的任务,使用者take已完成的任务,并按照完成这些任务的顺序处理他们的结果。

  4. Callable 返回结果并且可能抛出异常的任务。实现call方法。如果返回结果为空则,runnable无法抛异常和返回结果

  5. Executor执行已提交的Runnable任务的对象,此接口提供一种将任务提交与每个任务将如何运行的机制分离开来的方法。

  6. ExecutorService提供了任务的生命周期管理的方法。

  7. Executor框架将任务的提交与执行策略解耦开来,同时还支持多种不同类型的执行策略。

  8. 在Javs的API或语言规范中,并没有将中断与任何取消语义关联起来,但实际上,如果在取消之外的其他操作中使用中断,那么都是不合适的,并且很难支撑起更大的应用。调用iterrupt并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的请求。通常中断时实现取消的最合理方式。

  9. 由于每个线程拥有各自的中断策略,因此除非你知道中断对该线程的含义,否则就不应该中断这个线程。只有实现了线程中断策略的代码才可以屏蔽中断请求。在常规的任务和库代码中都不应该屏蔽中断请求。相应中断的方法:传递异常,恢复中断状态Thread.currentThread().interrupt。

  10. 对于持有线程的服务,只要服务的存在时间大于创建线程的方法的存在时间,那么就应该提供生命周期方法。

  11. 在运行时间较长的应用程序中,通常会为所有线程的未捕获异常指定同一个异常处理器,并且该处理器至少会将异常信息记录到日志中。

  12. 守护线程通常不能用来代替应用程序管理程序中各个服务的生命周期。

  13. 在一些任务中,需要拥有或排除某种特定的执行策略。如果某些任务依赖于其他的任务,那么会要求线程池足够大,从而确保他们依赖的任务不会被放入等待队列中或被拒绝,而采用线程封闭机制的任务需要串行执行。通过将这些需求写入文档,将来的代码维护人员就不会由于使用了某种不合适的执行策略而破坏安全性或活跃性。

  14. 每当提交了一个有依赖性的Executor任务时,要清楚地知道可能会出现线程“饥饿”死锁,因此需要在代码或配置Executor的配置文件中记录线程池的大小限制或配置限制。

  15. 对于计算密集型的任务,在拥有N个处理器的系统上,当线程池的大小为N+1时,通常能实现最优的利用率。要正确设置线程池的大小,必须估算出任务的等待时间与计算时间的比值。

  16. 对于Executor,newCachedThreadPool工厂方法是一种很好的默认选择,它能提供比固定大小的线程池更好的排队性能。当需要限制当前任务的数量以满足资源管理需求时,那么可以选择固定大小的线程池,就像在接受网络客户请求的服务器应用程序中,如果不进行限制,那么很容易发生过载问题。只有当任务相互独立时,为线程池或工作队列设置界限蚕食合理的,如果任务之间存在依赖性,那么有界线程池或队列可能导致线程饥饿死锁问题。此时应使用无解线程池如newCachedThreadPool。

  17. SynchronousQueue是这样 一种阻塞队列,其中每个 put 必须等待一个 take,反之亦然。同步队列没有任何内部容量,甚至连一个队列的容量都没有。 不能在同步队列上进行 peek,因为仅在试图要取得元素时,该元素才存在;除非另一个线程试图移除某个元素,否则也不能(使用任何方法)添加元素;也不能迭代队列,因为其中没有元素可用于迭代。队列的头是尝试添加到队列中的首个已排队线程元素; 如果没有已排队线程,则不添加元素并且头为 null。对于其他 Collection 方法(例如 contains),SynchronousQueue 作为一个空集合。此队列不允许 null 元素。注意1:它一种阻塞队列,其中每个 put 必须等待一个 take,反之亦然。同步队列没有任何内部容量,甚至连一个队列的容量都没有。 注意2:它是线程安全的,是阻塞的。注意3:不允许使用 null 元素。注意4:公平排序策略是指调用put的线程之间,或take的线程之间。可以认为SynchronousQueue是一个缓存值为1的阻塞队列,但是 isEmpty()方法永远返回是true,remainingCapacity() 方法永远返回是0,remove()和removeAll() 方法永远返回是false,iterator()方法永远返回空,peek()方法永远返回null。

  18. LinkedBlockingQueue是无界的,是一个无界缓存的等待队列。此队列按照FIFO原则对元素进行排序。基于链表的阻塞队列,内部维持着一个数据缓冲队列(该队列由链表构成)。当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

  19. ArrayBlockingQueue是有界的,是一个有界缓存的等待队列。基于数组的阻塞队列,同LinkedBlockingQueue类似,内部维持着一个定长数据缓冲队列(该队列由数组构成)。此队列按照FIFO原则对元素进行排序。队列的头部是在队列中存在时间最长的元素,队列的尾部是在队列中存在时间最短的元素。新元素插入到队列的尾部,队列检索操作是从队列的头部开始获得元素。ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue;按照实现原理来分析ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。 ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。 ArrayBlockingQueue和LinkedBlockingQueue是两个最普通、最常用的阻塞队列,一般情况下,处理多线程间的生产者消费者问题,使用这两个类足以。

  20. 当串行循环中的各个迭代操作之间彼此独立并且每个迭代操作执行的工作量比管理一个新任务来带来的开销更多,那么这个串行循环就适合并行化。

  21. 如果所有线程以固定的顺序来获得锁,那么在程序中就不会出现锁顺序死锁问题。

  22. 如果在持有锁时调用某个外部方法,那么将出现活跃性问题。在这个外部方法中可能会获取其他锁(这可能会产生死锁),或者阻塞时间过长,导致其他线程无法及时获得当前持有的锁。

  23. 如果在调用某个方法是不需要持有锁,那么这种调用被称为开放调用。在程序中应尽量使用开放调用。与那些在持有锁时调用外部方法的程序相比,更易于对依赖于开放调用的程序进行死锁分析。

  24. 产生死锁的四个必要条件:(1) 互斥条件:一个资源每次只能被一个进程使用。(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立

  25. 要避免使用线程优先级,因为这会增加平台依赖性,并可能导致活跃性问题。在大多数并发应用程序中,都可以使用默认的线程优先级。

  26. 可伸缩性指的是:当增加计算资源时(例如CPU、内存、存储容量或者I/O带宽),程序的吞吐量或者处理能力能相应的增加。

  27. 避免不成熟的优化。首先使程序正确,然后再提高运行效率——如果它还运行的不够快

  28. 以测试为基准,不要猜测。

  29. Amdahl定律:定义了串行系统并行化后加速比的计算公式和理论上限。加速比定义:加速比=优化前系统耗时/优化后系统耗时。所谓加速比,就是优化前的耗时与优化后耗时的比值。加速比越高,表明优化效果越明显。Amdahl定律给出了加速比与系统并行度和处理器数量的关系。设加速比为Speedup,系统内必须串行化的程序比重为F,CPU处理器数量为N,则有:

$$\large Speedup\leq \frac{1}{F + \frac{1 – F}{N}}$$

根据这个公式,如果CPU处理器数量趋于无穷,那么加速比与系统的串行化率成反比,如果系统中必须有50%的代码串行执行,那么系统的最大加速比为2。

  1. 所有并发程序中都包含一些串行部分。如果你认为在你的程序中不存在串行部分,那么可以再仔细检查一遍

  2. 不要过度担心非竞争同步带来的开销。这个基本的机制已经非常快了,并且JVM还能进行额外的优化以进一步降低或消除开销。因此,我们应该将优化的重点放在那些发生竞争的地方。

  3. 在并发程序中,对可伸缩性的最主要威胁就是独占方式的资源锁。

  4. 有三种方式可以降低锁的竞争程度:

    1. 减少锁的持有时间
    2. 降低锁的请求频率
    3. 使用带有协调机制的独占锁,这些机制允许更高的并发性。
  5. 通常,对象分配操作的开销比同步的开销更低。

  6. 在构建对并发类的安全性测试中,需要解决的关键问题在于,要找出那些容易检查的属性,这些属性在发生错误的情况下极有可能失败,同时又不会使得错误检查代码人为地限制并发性。理想的情况是,在测试属性中不需要任何同步机制。

  7. 测试应该放在多处理器的系统上进行,从而进一步测试更多形式的交替运行。然而CPU的数量越多并不一定会是测试高效。要最大程度的检测出一些对执行时序敏感的数据竞争,那么测试中的线程数量应该多于CPU数量,这样在任意时刻都会有一些线程在运行,而另一些被交换出去,从而可以检查线程间交替行为的可预测性。

  8. Lock提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操作 。

  9. ReentrantLock实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。与synchronized相比,它还为处理锁的不可用性问题提供了更高的灵活性。

  10. 在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用RenentrantLock,这些功能包括:可定时的、可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用synchronized。