Java 多线程基础概念

一、线程安全

线程安全的定义:

  • 某个类的行为与其规范完全一致。
  • 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类就能表现出正确的行为,那么就称这个类是线程安全的
  • 无状态对象一定是线程安全的(多个线程访问无状态对象一定是线程安全的)

1. 原子性

原子操作定义: 对于访问同一个状态的所有操作(包括操作本省), 这个操作是以一个原子方式执行的操作。

  • 竞态条件
  1. 多个线程交替操作 Class 成员变量, 当出现不恰当的 执行时序(读取-修改-写入), 会导致不恰当的后果。这种后果叫做 "竞态条件 Race Condition"
  2. 当某个计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件。
  3. 本质是基于一种可能失效的观察结果来做出判断或者执行某个计算。
  4. 最常见的竞态条件类型就是“先检查后执行(Check-then-Act)。
  5. 读取-修改-写入”操作也是一种竞态条件。
  • 复合操作
  1. java.util.concurrent.atomic 包中, 包含了原子变量类,用于实现数值和对象引用上的原子切换。同这些原子变量类确保访问操作都是原子性。

2. 加锁机制

  • 内置锁
  1. 内置锁是一种互斥锁,最多只有一个线程持有这个锁。
  2. 每个Java对象都可以用作一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或者监视器锁(Monitor Lock)。
  • 重入
  1. 因为内置锁是可重入的,所以如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。
  2. 重入意味着获取锁的操作粒度是“线程”,而不是“调用”。
  3. 重入提升了加锁行为的封装性,因此简化了面向对象并发代码的执行

3. 用锁保护状态

  1. 锁能够使其被保护的对象以串行方式来执行。
  2. 对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。
  3. 对象的内置锁与其状态之间没有内在的联系,虽然大多数类都将内置锁用做一种有效的加锁机制,但对象的域并不一定要通过内置锁来保护。
  4. 一种常见的加锁约定:将所有可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得该对象上不会发生并发访问。
  5. 对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。

4. 活跃性与性能

  1. 将同步代码块分解得过细并不好,因为获取与释放锁操作需要开销
  2. 当执行时间较长的计算或者可能无法快速完成的操作时(如I/O),一定不要持有锁。
  3. 同时使用两种不同的同步机制会带来混乱,在性能或安全性上也没有任何好处。(如内置锁synchronizedAtomic原子变量)

二. 对象的共享与同步

同步代码块和同步方法: 可以确保原子的方式执行操作

synchronized 同步: 不仅实现了原子性、临界区(Critical Section),还实现了内存可见性(Memory Visibility)

1. 对象可见性

多线程环境中的问题: 在多线程环境中某个变量被线程修改, 但是其他线程看不到这个修改的变量内容. 如下

  • 失效的数据
  1. 比如两步赋值操作,赋值的顺序可能会跟看到的顺序相反。
  2. 在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确的结论。
  • 非原子 64 位操作, 最低安全性
  1. 最低安全性: 当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全性保证也被称为最低安全性(out-of-thin-airsafety
  2. 最低安全性适用于绝大多数变量,当时存在一个例外:非volatile类型的64位数值变量。Java内存模型要求,变量的读取操作和写入操作都必须是原子操作,但对于非volatile类型的longdouble变量,JVM允许将64位的读操作或写操作分解为两个32位的操作
  • 加锁与可见性
  1. Java 内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果
  2. 加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的"最新值",所有执行读操作或者写操作的线程都必须在同一个锁上同步。
  • Volatile 变量
  1. Java 提供了一种稍弱的同步机制, volatile 变量, 主要用来确保将变量的更新操作通知到其他线程。
  2. 加锁机制既可以保证可见性又可以保证原子性,而 volatile 变量只能确保可见性
  3. 可见性:在读取volatile类型的变量时总会返回最新写入的数据。
  4. 禁止指令重排序:变量被声明为 volatile 类型后, 编译器与运行时都会注意这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。
  5. volatile 仅保证可见性,无法保证线程安全性

2. 对象发布与逸出

  • 发布(Publish)

发布对象: 是指对象能够在当前作用域之外的代码中使用,比如将对象的引用保存到其他代码可以访问该对象的方法中。

  1. ①将对象的引用保存到一个公有的静态变量中,以便任何类和线程都能看到该对象;
  2. // 公共的静态变量中, 发布对象
  3. public static Set<string> mySet;
  4. public void initialize() {
  5. mySet = new HashSet<string>();
  6. }
  7. }
  8. ②发布某个对象的时候,在该对象的非私有域中引用的所有对象都会被发布;
  9. public class UnsafeStates {
  10. private String[] states = new String[] {
  11. "CN", "US"
  12. };
  13. // 在对象非私有域中引用ui想会被发布
  14. public String[] getStates() {
  15. return status;
  16. }
  17. }
  18. ③发布一个内部的类实例,内部类实例关联一个外部类引用。
  19. public class ThisEscape {
  20. public ThisEscape(EventSource source) {
  21. source.registerListener(new EventListener() {
  22. public void onEvent(Event e) {
  23. doSomething(e);
  24. }
  25. });
  26. }
  27. }
  • 逸出 - 对象逸出

常见错误:在构造函数中启动一个线程. 线程安全对象构造过程如下

  1. 1) 在对象构造期间,不要公布this引用
  2. 2) 不要在构造方法中使用 this 引用逸出
  3. 3) 不要从构造函数内启动线程
  4. a) 在构造函数中启动线程时,构造函数还未执行完毕,不能保证此对象已经完全构造
  5. b) 如果在启动的线程中访问此对象,不能保证访问到的是完全构造好的对象
  • 对象安全发布
  1. 1) Java 中存在三种对象
  2. a) 不变对象:对象的所有域为final,对象状态创建后不能再修改,对象是正确构造的
  3. b) 基本不变对象:不满足不变对象的约束,但是初始化后不再变化
  4. c) 可变对象:不满足上述不变对象和基本不变对象的约束
  5. 2) 安全发布技术
  6. a) 即确保对象引用和状态对其他线程正确可见
  7. b) 方式
  8. 静态初始化器初始化对象引用
  9. 将引用存储到volatile
  10. 将引用存储到正确创建对象的final
  11. 3) 三种对象安全发布方式
  12. a) 不变对象:任何形式机制发布
  13. b) 基本不变对象:保证安全发布即可
  14. c) 可变对象:不仅要保证安全发布,而且要确保对象状态的正确改变.即用锁或其他方式,保证对象状态的正确改变.
  15. 4) 示例
  16. ThisEscape 中给出了逸出的一个特殊示例,即 this 引用在构造函数中逸出。内部 EventListener 实例发布时,在外部封装的 ThisEscape 实例也逸出了。当且仅当对象的构造函数返回时,对象才处于可预测的和一致的状态。
  17. 因此,当从对象的构造函数中发布对象时,只是发布了一个尚未构造完成的对象。即使发布对象的语句位于构造函数的最后一行也是如此。如果 this 引用在构造过程中逸出,那么这种对象就被认为是不正确构造。
  18. 在构造过程中使 this 引用逸出的一个常见错误是,在构造函数中启动一个线程。当对象在其构造函数中创建一个线程时,无论是显示创建(通过将它传给构造函数)还是隐式创建(由于 Thread Runnable 是该对象的一个内部类), this 引用都会被新创建的线程共享。
  19. 在对象尚未被创建完成之前,新的线程就可以看见它。在构造函数中创建线程并没有错误,但最好不要立即启动它,而是通过一个 start initialize 方法来启动。在构造函数中调用一个可改写的实例方法时,同样会导致 this 引用在构造过程中逸出。
  20. 如果想在构造函数中注册一个事件监听器或启动线程,那么可以使用一个私有的构造函数和一个公共的工厂方法(Factory Method),从而避免不正确的构造过程,如下面的 SafeListener
  21. public class SafeListener{
  22. private final EventListener listener;
  23. private SafeListener(){
  24. listener = new EventListener(){
  25. public void onEvent(Event e){
  26. doSomething(e);
  27. }
  28. };
  29. }
  30. public static SafeListener newInstance(EventSource source){
  31. SafeListener safe = new SafeListener();
  32. source.registerListener(safe.listener);
  33. return safe;
  34. }
  35. }

3. 线程封闭技术(Thread Confinement)

当访问共享的可变数据数据时,通常需要使用同步。一种避免同步的方式就是不共享数据。

如果仅在单线程内访问数据,就不需要同步,这种技术被称为线程封闭(Thread Confinement),也是线程安全最简单的方式之一。线程封闭应用场景: JDBC(Java Database Connectivity) 的 Connectivity 连接对象

  • Ad-hoc 线程封闭技术
  1. 1) Ad-hoc 线程封闭:
  2. 维护线程封闭性的职责完全由程序实现来承担。但在实际场景中, 对线程封闭对象的引用通常保存在共有的变量中。在某些情况下,单线程子系统提供的简单性要胜过 Ad-hoc 线程封闭技的脆弱性。另一个原因是在使用单线程子系统可以避免死锁。
  3. 2) volatile 变量一个特殊的线程封闭:
  4. 能确保只有单个线程对共享的 volatile 变量执行写入操作(其他线程有读取 volatile 变量),那么就可以安全地在这些共享的 volatile 变量上执行“读取-修改-写入”的操作,而其他读取 volatile 变量的线程也能看到最新的值。
  • 栈 线程封闭技术
  1. 1) 栈封闭中:
  2. 只能通过局部变量才能访问对象。
  3. 2) 注意事项:
  4. 在使用 栈线程封闭性 时,架构师需要确保被引用的对象不会逸出到方法外。
  5. // 对于基本类型的局部变量,例如下面 loadTheArk 方法的 numPairs ,无论如何都不会破坏栈封闭性。由于任何方法都不发获得对基本类型的引用,因此 Java 语言的这种语义确保了基本类型的局部变量始终封闭在线程内。
  6. public int loadTheArk(Collection candidates) {
  7. SortedSet animals;
  8. int numPairs = 0;
  9. Animal candidate = null;
  10. // animals被封闭在方法中,不要使它们逸出!
  11. animals = new TreeSet(new SpeciesGenderComparator());
  12. animals.addAll(candidates);
  13. for (Animal a : animals) {
  14. if (candidate == null || !candidate.isPotentialMate(a))
  15. candidate = a;
  16. else {
  17. ark.load(new AnimalPair(candidate, a));
  18. ++numPairs;
  19. candidate = null;
  20. }
  21. }
  22. return numPairs;
  23. }
  • ThreadLocal 类 线程封闭技术
  1. 1) ThreadLocal 是线程封闭的更规范方法,这个类能使线程中的某个值与保存值的对象关联起来。
  2. 2) get() set() 方法
  3. 这些方法为每个使用该变量的线程都存有一份独立的副本,因此 get 总是返回由当前执行线程在调用 set 时设置的最新值。当某个线程初次调用 ThreadLocal.get 方法时,就会调用 initialValue 来获取初始值。从概念上看,你可以将 ThreadLocal 视为包含了 Map<Thread, T> 对象,其中保存了特定于该线程的值,但 ThreadLocal 的实现并非如此。这些特定于线程的值保存在 Thread 对象中,当线程终止后,这些值会作为垃圾回收。
  4. 3) ThreadLocal 对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。示例:
  5. 在单线程应用程序中可能会维持一个全局的数据库连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法时都要传递一个Connection对象。由于JDBC的连接对象不一定是线程安全的,因此,当多线程应用程序在没有协同的情况下使用全局变量时,就不是线程安全的。通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接。
  6. // 在 ThreadLocal 黄建线程安全类
  7. private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
  8. public Connection initialValue() {
  9. return DriverManager.getConnection(DB_URL);
  10. }
  11. };

4. 对象可变/不变性

  • 不变性
  1. 1) 满足同步需求的另一种方法是使用 不可变对象(Immutable Object)。不可变对象一定是线程安全的。
  2. 2) 当满足以下这些条件的时候,对象才是不可变的.
  3. a) 对象创建以后其状态就不能被修改;
  4. b) 对象的所有域都是 final 类型;
  5. c) 对象是正确创建的(在对象的创建期间,this 引用没有逸出)。
  6. 3) 不变性编程注意事项
  7. a) 除非使用更高的可见性,否则应将所有的域都声明为 private
  8. b) 除非需要某个域是可变的,否则都应该声明为 final
  • Final 域
  1. 1) final 用于构造不可变对象, 是不能修改的
  2. 2) Java 内存模型中, final 域能够确保初始化过程的安全性
  3. @Immutable
  4. public class OneValueCache {
  5. private final BigInteger lastNumber;
  6. private final BigInteger[] lastFactors;
  7. public OneValueCache(BigInteger i,
  8. BigInteger[] factors) {
  9. lastNumber = i;
  10. lastFactors = Arrays.copyOf(factors, factors.length);
  11. }
  12. public BigInteger[] getFactors(BigInteger i) {
  13. if (lastNumber == null || !lastNumber.equals(i))
  14. return null;
  15. else
  16. return Arrays.copyOf(lastFactors, lastFactors.length);
  17. }
  18. }
  • Volatile 类型发布不可变对象
  1. volatile 仅保证可见性,无法保证线程安全性
  2. @ThreadSafe
  3. public class VolatileCachedFactorizer implements Servlet {
  4. private volatile OneValueCache cache = new OneValueCache(null, null);
  5. public void service(ServletRequest req, ServletResponse resp) {
  6. BigInteger i = extractFromRequest(req);
  7. BigInteger[] factors = cache.getFactors(i);
  8. if (factors == null) {
  9. factorfactors = factor(i);
  10. cache = new OneValueCache(i, factors);
  11. }
  12. encodeIntoResponse(resp, factors);
  13. }
  14. }

5. 对象安全发布

  • 不正确的发布: 正确对象被破坏

  • 不可变对象与初始化安全性

  • 安全发布的常用模式

  1. 1) 可变对象必须以安全的方式发布,这就意味着发布和使用该对象的线程时都必须使用"同步"
  2. 2) 要安全的发布一个对象,对象的引用以及对象的状态必须同时对其它线程可见。
  3. 3) 一个正确构造的对象可以通过以下 "几种方式" 来安全地发布:
  4. a) 在静态初始化函数中初始化一个对象引用
  5. b) 将对象的引用保存到 volatile 类型的于或者 AtomicReferance 对象中
  6. c) 将对象的引用保存到某个正确构造对象的 final 类型域中
  7. d) 将对象的引用保存到一个由锁保护的域中
  • 事实不可变对象 Effectively (Immutable ojbects)
  1. 1) 一个不可变的对象必须满足的条件:
  2. 它的状态在创建后不能再修改,所有域都是 final 类型,并且它被正确创建(创建期间没有发生this引用的逸出)
  3. 2) 对象从技术上看是可变的,但其状态在发布后不会再改变,那么把这种对象成为 "事实不可变对象(Effectively Immutable Object)"
  4. 3) 再没有额外的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。
  5. 4) 例如 Data 本身是可变的,如果将 Date 当作不可变对象来使用,又要省去对锁的使用:
  6. public Map<String, Date> lastLogin = Collections.synchronizedMap(new HashMap<String, Date>() );
  • 可变对象 (mutable Objects)
  1. 1) 对象在构造后可以修改,安全发布只能确保 "发布当时" 是安全的。
  2. 2) 对象发布需求取决与它的可变性(划重点)
  3. a) 不可变对象, 可以通过任意机制发布
  4. b) 事实不可变对象, 必须通过安全方式发布
  5. c) 可变对象, 必须通过安全方式发布,并且必须是线程安全的或者由某个锁保护起来的
  • 安全地共享对象
  1. 在并发程序中使用和共享对象时,可以使用一些使用的策略,包括:
  2. 1) 线程封闭
  3. 线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
  4. 2) 只读共享
  5. 在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但很任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
  6. 3) 线程安全共享
  7. 线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步欧。
  8. 4) 保护对象
  9. 被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。

三. 对象的组合

1. 设计线程安全的类

  1. 1) 设计线程安全类的过程中,需要包含以下三个基本要素
  2. a) 找出构成对象状态的所有变量(字段)
  3. b) 找出约束状态变量的不变性条件
  4. c) 建立对象状态的并发访问管理策略
  5. 2) 对象的状态
  6. a) 如果所有的域都是基本类型,则这些域构成对象的全部状态;
  7. b) 如果包含其他对象,该对象的状态将包括被引用对象的域。
  8. 3) 同步策略规定了如何将不变性条件、线程封闭和加锁机制结合起来以维护线程的安全性,并且规定了哪些变量由哪些锁来保护
  • 收集同步需求
  1. 1) 尽量多的使 final 域。 final 类型的域使用的越多,状态空间就越小,越能简化对象可能状态的分析过程。
  2. 2) 在单个或多个域(字段)上,某一个操作如果存在的无效的状态转换,需要对该操作进行同步. 无效的状态转换包括不满足
  3. 3) 其他
  4. 不变性条件例如:int 变量超出最大最小值。
  5. 后验条件,例如:counter 当前值 17,那么下一个操作结束一定是 18.
  6. 如果某个操作存在无效的状态转换,那么该操作必须是原子的,即需要同步。
  • 依赖状态的操作
  1. 某些方法包含一些先验条件才能执行
  2. 例如:不能够从空队列中删除一个值。单线程程序中如果遇到无法满足先验条件的情况可以直接返回失败,但是并发程序中先验条件可能因为其他线程的执行而变成真,因此要一直等待先验条件为真再执行。
  • 分析状态的所有权
  1. 所有权 Java 中只是一个设计中的要素,在语言层面没有明显的变现。所有权意味着控制权,如果发布了某个可变对象的引用,则意味着共享控制权。在定义哪些变量构成对象的状态时,只考虑对象拥有的数据。

2. 实例封闭

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

  • Java 监视器模式
  1. 1) 遵循 Java 监视器模式的对象会把对象的所有可变状态都封装起来,并由自己的内置锁保护
  2. public final class Counter {
  3. @GuardedBy("this") private long value = 0;
  4. public synchronized long getValue() {
  5. return value;
  6. }
  7. public synchronized long increment() {
  8. if (value == Long.MAX_VALUE)
  9. throw new IllegalStateException("counter overflow");
  10. return ++value;
  11. }
  12. }
  13. 2) Counter 中封装了一个变量 value,对该变量的所有访问都需要通过 Counter 的方法执行,这些方法都是同步的。私有私有锁保护状态.
  14. public class PrivateLock {
  15. private final Object myLock = new Object();
  16. @GuardedBy("myLock") Widget widget;
  17. void someMethod() {
  18. synchronized (myLock) {
  19. // Access or modify the state of widget
  20. }
  21. }
  22. }
  • 实例:车辆追踪

3. 线程安全性的委托

  • 示例: 基于委托的车辆追踪器
  • 独立的状态变量
  • 当委托失效时
  1. 如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性委托给底层的状态变量。
  • 发布底层的状态变量
  1. 如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么久可以安全地发布这个变量
  • 示例:发布状态的车辆追踪器

4. 现有线程安全类中添加功能

  • 客户端加锁机制
  1. 将加锁代码分布在多个类中,破坏同步策略的封装性。
  2. 客户端加锁: 以给一个 list putIfAbsent 功能为例
  3. class GoodListHelper <E> {
  4. public List<E> list = Collections.synchronizedList(new ArrayList<E>());
  5. public boolean putIfAbsent(E x) {
  6. synchronized (list) {
  7. boolean absent = !list.contains(x);
  8. if (absent)
  9. list.add(x);
  10. return absent;
  11. }
  12. }
  13. }
  • 组合(Composition)
  1. 当为现有类加一个原子操作时,有更好的方法:组合
  2. public class ImprovedList<T> implements List<T> {
  3. private final List<T> list;
  4. /**
  5. * PRE: list argument is thread-safe.
  6. */
  7. public ImprovedList(List<T> list) { this.list = list; }
  8. public synchronized boolean putIfAbsent(T x) {
  9. boolean contains = list.contains(x);
  10. if (contains)
  11. list.add(x);
  12. return !contains;
  13. }
  14. // Plain vanilla delegation for List methods.
  15. // Mutative methods must be synchronized to ensure atomicity of putIfAbsent.
  16. ...
  17. }

5. 将同步策略文档化

四. 基础构建模块

1. 同步容器类

  • 同步容器类的问题
  1. 1) 同步容器:可以简单地理解为通过synchronized来实现同步的容器,比如VectorHashtable以及SynchronizedList等容器,如果有多个线程调用同步容器的方法,它们将会串行执行。
  2. 2) 线程安全实现的方式:将他们的可变成员变量封装起来,并对每个方法都进行同步,使得每次仅仅有一个线程能访问这些可变的成员变量。
  3.   
  4. 3) 尽管这些类的方法都是同步的,但当并发访问多个方法的时候,还是有可能出错。
  5. 比如,有两个线程,一个执行同步的 get 方法,一个执行同步的 remove 方法,那么这两个线程仍然可能出现竞态条件(多个线程不同的时序导致程序出问题):"先 remove 再 get 就会出现问题",在使用的时候仍然要注意。
  • 迭代器与 Concurrent-ModificationException
  1. ConcurrentModificationException 异常:
  2. 如果有其他线程并发的修改容器,就要在迭代期间对容器加锁。
  3.  当迭代器发现容器在迭代过程中被修改时,就会抛出一个 ConcurrentModificationException 异常。
  4.  具体过程:将计数器的变化与容器关联起来,如果在迭代期间计数器被修改,那么 hasNext() next() 将抛出如上异常。
  5.  如果在迭代期间不希望对容器加锁,那么一种替代方法就是"克隆"容器,并在副本上进行迭代。
  • 隐藏迭代器
  1. 1) 调用了 vector toString() 函数,这个函数就是一个隐藏的迭代过程。如果在这个过程中,一个线程获得了 CPU并且执行了remove()方法,也会报告 ConcurrentModificationException 异常。
  2. 2) hashCode equals 也会间接执行迭代操作。
  3. 3) 所以在所有对共享容器进行迭代的地方都要加锁。

2. 并发容器

java 5.0 提供了多种并发容器类来改进同步容器类的性能。同步容器将所有对容器状态的访问都串行化,以实现线程安全。严重降低并发性,当多个线程竞争容器的锁时,吞吐量将严重降低。通过并发容器来代替同步容器,可以极大的提高伸缩性并且降低安全性风险。

  • ConcurrentHashMap
  1. 大家都知道 HashMap 是非线程安全的,Hashtable 是线程安全的,但是由于 Hashtable 是采用 synchronized 进行同步,相当于所有线程进行读写时都去竞争一把锁,导致效率非常低下。
  2. ConcurrentHashMa p可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,不用对整个 ConcurrentHashMap 加锁。ConcurrentHashMap 为了提高本身的并发能力,在内部采用了一个叫做 Segment 的结构,一个 Segment 其实就是一个 Hash Table 的结构,Segment 内部维护了一个链表数组,我们用下面这一幅图来看下 ConcurrentHashMap 的内部结构
  • ConcurrentLinkedQueue
  1. ConcurrentLinkedQueue 使用链表作为数据结构,它采用无锁操作,可以任务是高并发环境下性能最好的队列。
  2. ConcurrentLinkedQueue 是非阻塞线程安全队列,无界,故不太适合做生产者消费者模式,而 LinkedBlockingQueue 是阻塞线程安全队列,可以做到有界,通常用于生产者消费者模式。
  • CopyOnWriteArrayList
  1. 用于替代同步List,在迭代期间不需要对容器进行加锁或复制。(类似,CopyOnWriteArraySet代替同步set)。
  2. 写入时复制(Copy-On-Write)容器的线程安全性在于,只要是正确发布一个事实不可变的对象,那么在访问该对象时就不再需要进一步的同步。在每次修改时,都会创建并重新发布一个新的容器副本,从而实现可变性。
  3. 每次修改容器都会复制底层数组,需要一定的开销。仅当迭代的操作远远多于修改操作时,才应该使用“写入时复制“的容器
  4. Copy-On-Write 简称 COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用 CopyOnWrite 机制实现的并发容器,它们是 CopyOnWriteArrayList CopyOnWriteArraySetCopyOnWrite 容器非常有用,可以在非常多的并发场景中使用到。
  5. CopyOnWrite 容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以 CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器

3. 阻塞队列和生产者 - 消费者模式

  1. 1) BlockingQueue 阻塞队列提供可阻塞的 put take 方法,以及支持定时的 offer poll 方法。如果队列已经满了,那么 put 方法将阻塞直到空间可用;如果队列为空,那么 take 方法将阻塞直到有元素可用。
  2. 2) 队列可以是有界的也可以是无界的。
  3. 3) 阻塞队列提供了一个 offer 方法,如果数据项不能被添加到队列中,那么将返回一个失败状态。
  4.  
  5. 4) 在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具:它们能抵制并防止产生过多的工作项,使应用程序在负荷过载的情况下变得更加健壮。
  6. 5) BlockingQueue的多种实现:
  7.  a) LinkedBlockingQueueArrayBlockingQueue:是FIFO,二者分别与LinkedListArrayList类似,但比同步List拥有更好的并发性能。
  8. b) PriorityBlockingQueue:是一个按优先级排序的队列,当你希望按照某种顺序而不是FIFO来处理元素时,这个队列将非常有用。
  9. c) SynchronousQueue:事实上它并不是一个真正的队列,因为它不会为队列中元素维护存储空间。与其他队列不同的是,它维护一组线程,这些线程在等待这把元素加入或移出队列。如果以洗盘子为比喻,就相当于没有盘架来暂时存放洗好的盘子,而是将洗好的盘子直接放入下一个空闲的烘干机中。
  • 串行线程封闭
  1. 1) 优点: 对于可变对象,生产者-消费者这种设计与阻塞队列一起,促进了串行线程封闭,从而将对象所有权从生产者交付给消费者。线程封闭对象只能由单个线程拥有,通过安全地发布该对象“转移”所有权,实现了转移前由前一线程独占,转移后由后一线程独占。
  2. 2) 实现方法: 阻塞队列使得这种线程封闭的所有权转移变得容易,其次还可以通过ConcurrentMa p的原子方法 remove 或者 AtomicReference 的原子方法 compareAndSet 来完成这项工作。
  • 双端队列与工作密取
  1. 1) Java 6 增加了两种容器类型,Deque BlockingDeque 这两种容器类型,分别对 Queue BlockingQueue 进行了扩展。
  2. 2) Deque是一个双端队列,实现了在队列头和队列尾的高效插入和移除。具体实现包括ArrayDequeLinkedBlockingDeque
  3. 3) 在生产者-消费者模式中,所有消费者有一个共享的工作队列,而在工作密取设计中,每个消费者都有各自的双端队列。如果一个消费者完成了自己双端队列中的全部工作,那么它可以从其他消费者双端队列末尾秘密地获取工作。
  4. 4) 密取工作模式比传统的生产者-消费者模式具有更高的可伸缩性,这是因为工作者线程不会在单个共享的任务队列上发生竞争。这是因为工作者线程不会在单个共享的任务队列上发生竞争。在大多数情况下,它们都只是访问自己的双端队列,从而极大地减少了竞争。当工作者线程需要访问另一个队列时,它会从队列的尾部而不是头部获取工作,因此进一步降低了队列上的竞争程度。

4. 阻塞方法与中断方法

  • 阻塞方法
  1. 1) 线程可能在执行过程中阻塞或者暂停执行,例如等待IO结束,等待获得一个锁,等待从Thread.sleep方法中醒来,或者等待另一个线程的计算结果
  2. 2) 阻塞方法:BlockingQueueputtake方法会抛出收检查异常:InterruptedException。还有类似的方法如Thread.sleep。抛出InterruptedException的方法叫做阻塞方法。如果这个方法被中断,他将努力提前结束阻塞状态。
  • 中断方法
  1. 1) 中断是一种协作机制,一个线程不能强制要求其他线程停止正在执行的操作而去执行其他操作。当线程A中断线程B时,A只是要求B在执行到某个可以暂停的地方停止正在执行的操作。但是实际怎样处理中断是由线程B自己决定的。所以在,类中调用阻塞方法时,需要添加中断处理。
  2. 2) 什么时候会发生中断,
  3. a) 点击某个桌面应用中的取消按钮时;
  4. b) 某个操作超过了一定的执行时间限制需要中止时;
  5. c) 多个线程做相同的事情,只要一个线程成功其它线程都可以取消时;
  6. d) 一组线程中的一个或多个出现错误导致整组都无法继续时;
  7. e) 当一个应用或服务需要停止时
  8. 3) 什么时候可以中断
  9. 只有阻塞方法才可以中断,因为它提供了中断响应的策略。中断后不会产生问题。但是在非中断方法中,比如线程在执行一个排序算法,那么在排序过程中并不会发生中断,因为此时中断会产生问题即排序的数组中一部分有序一部分无序。此时如果在排序排序过程中必须处理才能中断。此时如果要强制结束线程必须调用 Thread.stop() Thread.destroy(),但是这样强制结束线程是不安全的。
  10. // 当线程中断时,还是会继续输出 2 和 3.
  11. public class InterruptedExample implements Runnable {
  12. @Override
  13. public void run() {
  14. System.out.println("1");
  15. Thread.currentThread().interrupt();
  16. System.out.println("2");
  17. System.out.println("3");
  18. }
  19. public static void main(String[] args) {
  20. Thread interruptedThread = new Thread(new InterruptedExample());
  21. interruptedThread.start();
  22. }
  23. }
  24. 4) 中断处理策略
  25. a) 传递InterruptedException:将中断处理交给方法的调用者,包括根本不捕获异常或者捕获异常后进行简单的处理之后再抛出异常。
  26. b) 恢复中断:有时候不能抛出InterruptedException,例如在Runnable中,这时 需要捕获异常并且恢复异常。
  27. c) 屏蔽中断:中断发生后捕获异常但是什么都不做。
  28. 5) 中断方法
  29. a) public void interrupt():中断线程
  30. b) public static boolean interrupted():查询当前线程中断状态,如果已经中断,就清除中断。
  31. c) public boolean isInterrupted():查询当前线程中断状态,但是不会改变中断状态。

5. 同步工具类

  • 闭锁
  1. CountDownLatch 闭锁:可以延迟线程的进度,直到锁到达终止状态。闭锁的作用相当于一扇门,在锁到达终止状态之前这扇门一直是关闭的。当锁到达终止状态时,允许所有线程通过。CountDownLatch 有一个初始值,通过调用 countDown 可以减少该值,一直到 0 时到达终止状态
  • FutureTask
  1. FutureTask 也可以叫做闭锁,FutureTask 表示的计算时通过 Callable 来实现的,相当于一种可生成 Runnable, 并且可以处于 3 种状态: 等待运行(Waiting to run) 用于执行一个可返回结果的长任务,任务在单独的线程中执行,其他线程可以用 get 方法取任务结果,如果任务尚未完成,线程在 get 上阻塞
  • 信号量
  1. Semaphore 用于控制同时访问某资源,或同时执行某操作的线程数目。信号量有一个初始值即可以分配的信号量总数目。线程任务开始前先调用 acquire 取得信号量,任务结束后调用 release 释放信号量。在 acquire 是如果没有可用信号量,线程将阻塞在 acquire 上,直到其他线程释放一个信号量
  • 棚栏
  1. CyclicBarrier 栅栏用于多个线程多次迭代时进行同步,在一轮任务中,任何线程完成任务后都在 barrier 上等待,直到所有其他线程也完成任务,然后一起释放,同时进入下一轮迭代

6. 构建高效且可伸缩的结果缓存