Java 多线程 - 高级特性

一. 显示锁 与 隐式锁(内置锁)

协调对共享对象的访问机制有: synchronized、volatile、ReentrantLock

1. Lock 与 ReentrantLock (显示锁)

  1. 1) Lock 接口中定义了一种无条件、可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁的方法都是显式的
  2. public interfece Lock {
  3. void lock();
  4. void lockInterruptibly() throws InterruptedException;
  5. boolean tryLock();
  6. boolean tryLock(long timeout, TimeUnit unit
  7. throw InterruptedException;
  8. void unlock();
  9. Condition newCondition();
  10. }
  11. 2) ReentrantLock 实现了 Lock 接口, 并提供了与 synchronized 相同的互斥性和内存可见性
  12. // 使用 ReentrantLock 保护对象状态
  13. Lock lock = new ReentrantLock();
  14. ...
  15. lock.lock();
  16. try {
  17. // 更新对象状态
  18. // 捕获异常,并在必要时恢复不变性条件
  19. } finally {
  20. // 一定要记得在 finally 块里释放锁
  21. lock.unlock();
  22. }
  23. 注意:在使用 ReentrantLock 时,一定要有释放锁的操作。
  24. ReentrantLock 不能完全替代 synchronized 的原因: 当程序执行控制离开被保护的代码块时,不会自动清除锁。因为人可能会忘记。
  • 轮询锁与定时锁
  1. 1) 轮询锁和定时锁可由tryLock来实现
  2. 2) 轮询锁,定时锁可以避免死锁的发生
  3. 3) 轮询锁通过释放已获得的锁,并退回重新尝试获取所有锁(lock.tryLock()),定时锁通过释放已获得的锁,放弃本次操作(lock.tryLock(timeout, unit))来避免死锁
  4. 4) 轮询锁: 通过 tryLock 来避免锁顺序死锁
  5. while (true) {
  6. if (fromAcct.lock.tryLock()) {
  7. try {
  8. if (toAcct.lock.tryLock()) {
  9. try {
  10. if (fromAcct.getBalance().compareTo(amount) < 0)
  11. throw new InsufficientFundsException();
  12. else {
  13. fromAcct.debit(amount);
  14. toAcct.credit(amount);
  15. return true;
  16. }
  17. } finally {
  18. toAcct.lock.unlock();
  19. }
  20. }
  21. } finally {
  22. fromAcct.lock.unlock();
  23. }
  24. }
  25. if (System.nanoTime() < stopTime)
  26. return false;
  27. NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod);
  28. }
  29. }
  30. 5) 定时锁: 带有时间限制的加锁
  31. public boolean trySendOnSharedLine(String message,
  32. long timeout, TimeUnit unit)
  33. throws InterruptedException {
  34. long nanosToLock = unit.toNanos(timeout)
  35. - estimatedNanosToSend(message);
  36. if (!lock.tryLock(nanosToLock, NANOSECONDS))
  37. return false;
  38. try {
  39. return sendOnSharedLine(message);
  40. } finally {
  41. lock.unlock();
  42. }
  43. }
  • 可中断的锁获取操作
  1. 1) Lock.lockInterruptibly():该锁与lock相似,但可以被中断
  2. 2) 如果线程未被中断,也不能获取到锁,就会一直阻塞下去,直到获取到锁或发生中断请求
  3. 3) 定时的 lock.tryLock(timeout, unit) 同样能响应中断
  • 非块结构加锁
  1. 1) 内置锁是基于块结构的加锁
  2. 2) Lock 可以使块与块交叉实现非块结构的加锁(连锁式加锁或者锁耦合),例:链表中,next 节点加锁后,释放 pre 节点的锁

2. ReentrantLock - 性能考虑因素

  1. 1) 竞争性能是可伸缩性的关键因素:如果有越多的资源被耗费在锁的管理和调度上,那么应用程序得到的资源就越少
  2. 2) Java5.0 中,ReentrantLock 能提供更高的吞吐量,但在 Java6 中,二者的吞吐量非常接近

3. ReentrantLock - 公平锁与非公平锁

  1. 1) ReentrantLock 构造函数中提供两种公平性选择
  2. 2) 公平锁与非公平锁
  3. Lock fairLock = new ReentrantLock(true); // 公平锁
  4. a) 在公平的锁上,线程将按照它们发出请求的顺序来获得锁
  5. b) 在非公平的锁上,则允许”插队“:当一个线程请求非公平的锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有的等待线程并获得这个锁
  6. c) 公平性将由于在挂起线程和恢复线程时存在的开销而极大地降低性能(非公平性的锁允许线程在其他线程的恢复阶段进入加锁代码块)
  7. d) 当持有锁的时间相对较长,或者请求锁的平局时间间隔较长,那么应该使用公平锁
  8. e) 内置锁默认为非公平锁

4. ReentrantLock(显示锁) 与 synchronized 隐式锁(内置锁) 之间进行选择

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

5. ReentrantLock 读 - 写锁

  1. 1) ReentrantLock 实现了一种标准的互斥锁: 每次最多只有一个线程能持有 ReentrantLock.
  2. a) 写-写: 互斥
  3. b) 写-读: 互斥
  4. 2) 读写锁的可选实现:
  5. a) 释放优先。写入锁释放后,应该优先选择读线程,写线程,还是最先发出请求的线程
  6. b) 读线程插队。锁由读线程持有,写线程再等待,再来一个读线程,是继续让读线程访问,还是让写线程访问
  7. c) 重入性。读取锁和写入锁是否可重入
  8. d) 降级。将写入锁降级为读取锁
  9. e) 升级。将读取锁升级为写入锁
  10. 3) 在非公平的锁中,线程获得访问许可的顺序是不确定的。写线程降级为读线程是可以的,当从读线程升级为写线程这是不可以的(这样会导致死锁)

6. ReentrantReadWriteLock

  1. 1) 读写锁的机制:
  2. a) 读-读不互斥,读线程可以并发执行;
  3. b) 读-写互斥,有写线程时,读线程会堵塞;
  4. c) 写-写互斥,写线程都是互斥的。
  5. 2) ReentrantReadWriteLock ReentrantLock 的比较:
  6. ReentrantReadWriteLock 是对 ReentrantLock 的复杂扩展,能适合更加复杂的业务场景,ReentrantReadWriteLock 可以实现一个方法中读写分离的锁的机制。而 ReentrantLock 只是加锁解锁一种机制。
  7. 2) 示例
  8. //创建ReentrantReadWriteLock对象
  9. private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
  10. //抽取读写锁
  11. private Lock readLock = rwl.readLock();
  12. private Lock writeLock = rwl.writeLock();
  13. public int getXXX(){
  14. readLock.lock();
  15. try{
  16. //执行操作
  17. }finally{
  18. readLock.unlock();
  19. }
  20. }
  21. public void setXXX(){
  22. writeLock.lock();
  23. try{
  24. //执行操作
  25. }finally{
  26. writeLock.unlock();
  27. }
  28. }

6. 对比

  1. 1) Synchronized 是在 JVM 层面上实现的,无需显示的加解锁,而 ReentrantLock ReentrantReadWriteLock 需显示的加解锁,一定要保证锁资源被释放
  2. 2) Synchronized 是针对一个对象的,而 ReentrantLock ReentrantReadWriteLock 是代码块层面的锁定
  3. 3) ReentrantReadWriteLock 引入了读写和并发机制,可以实现更复杂的锁机制,并发性相对于 ReentrantLock Synchronized 更高

二. 构建自定义同步工具

1. 状态依赖性管理

2. 使用条件队列

3. 显示 Condition 对象

4. Synchronizer

5. AbstractQueuedSynchronizer

6. java.util.concurrent 同步类中的 AQS

  1. AQS

三. 原子变量与非阻塞同步机制

1. 锁的劣势

2. 硬件对并发的支持

3. 原子变量类

4. 非阻塞算法

四. Java 内存模型

1. 内存模型

Java 内存模型的主要目标是定义程序中各个变量的访问规则, 即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节

  • 重排序

  • 顺序一致性

  • As-if-Serial

  • Happens-Before(发生之前) 规则

  1. 1) Happens-Before(发生之前) 规则
  2. Java 语言中有一个“先行发生”(happenbefore)的规则,它是 Java 内存模型中定义的两项操作之间的偏序关系,如果操作 A 先行发生于操作 B,其意思就是说,在发生操作 B 之前,操作A产生的影响都能被操作 B 观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等,它与时间上的先后发生基本没有太大关系。这个原则特别重要,它是判断数据是否存在竞争、线程是否安全的主要依据。
  3. 2) 举例: 假设存在如下三个线程,分别执行对应的操作:
  4. 线程 A 中执行如下操作:i=1
  5. 线程 B 中执行如下操作:j=i
  6. 线程 C 中执行如下操作:i=2
  7. 假设线程 A 中的操作”i=1 happenbefore 线程 B 中的操作“j=i”,那么就可以保证在线程 B 的操作执行后,变量 j 的值一定为 1,即线程 B 观察到了线程 A 中操作“i=1”所产生的影响;
  8. 现在,我们依然保持线程 A 和线程 B 之间的 happenbefore 关系,同时线程 C 出现在了线程 A 和线程 B 的操作之间,但是 C B 并没有 happenbefore 关系,那么 j 的值就不确定了,线程 C 对变量 i 的影响可能会被线程 B 观察到,也可能不会,这时线程 B 就存在读取到不是最新数据的风险,不具备线程安全性。
  9. 2) Java 内存模型中的八条可保证 Happens-Before(发生之前) 的规则,它们无需任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们进行随机地重排序。
  10. a) 程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作 happenbefore(时间上)后执行的操作。
  11. b) 管理锁定规则:一个 unlock 操作 happenbefore 后面(时间上的先后顺序,下同)对同一个锁的 lock 操作。
  12. c) volatile 变量规则:对一个 volatile 变量的写操作 happenbefore 后面对该变量的读操作。
  13. d) 线程启动规则:Thread 对象的 start()方法 happenbefore 此线程的每一个动作。
  14. e) 线程终止规则:线程的所有操作都 happenbefore 对此线程的终止检测,可以通过 Thread.join() 方法结束 Thread.isAlive() 的返回值等手段检测到线程已经终止执行。
  15. f) 线程中断规则:对线程 interrupt() 方法的调用 happenbefore 发生于被中断线程的代码检测到中断时事件的发生。
  16. g) 对象终结规则:一个对象的初始化完成(构造函数执行结束)happenbefore 它的 finalize() 方法的开始。
  17. h) 传递性:如果操作 A happenbefore 操作 B,操作 B happenbefore 操作 C,那么可以得出 A happenbefore 操作 C
  18. 3) 偏序关系
  19. 4) 全序关系
  • 主内存与工作内存
  1. 1) Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节
  2. 2) 此处的变量主要是指共享变量,存在竞争问题的变量。Java 内存模型规定所有的变量都存储在主内存中,而每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(根据 Java 虚拟机规范的规定,volatile 变量依然有共享内存的拷贝,但是由于它特殊的操作顺序性规定——从工作内存中读写数据前,必须先将主内存中的数据同步到工作内存中,所有看起来如同直接在主内存中读写访问一般,因此这里的描述对于 volatile 也不例外)。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值得传递均需要通过主内存来完成。
  • Java 内存模型中定义了以下 8 种操作来完成主内存与工作内存之间交互的实现细节
  1. luck(锁定):作用于主内存的变量,它把一个变量标示为一条线程独占的状态。
  2. unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  3. read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到工作内存中,以便随后的 load 动作使用。
  4. load(载入):作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
  5. use(使用):作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时将会执行这个操作。
  6. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  7. store(存储):作用于工作内存的变量,它把工作内存中的一个变量的值传递到主内存中,以便随后的 write 操作使用。
  8. write(写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量值放入主内存的变量中。
  • Java 内存模型还规定了执行上述 8 种基本操作时必须满足如下规则
  1. 不允许 read loadstore write 操作之一单独出现,以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read load 之间、store write 之间是可插入其他指令的。
  2. 不允许一个线程丢弃它的最近的 assign 操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
  3. 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从线程的工作内存同步回主内存中。
  4. 一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load assign)的变量,换句话说就是对一个变量实施 use store 操作之前,必须先执行过了 assign load 操作。
  5. 一个变量在同一个时刻只允许一条线程对其执行 lock 操作,但 lock 操作可以被同一个条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。
  6. 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load assign 操作初始化变量的值。
  7. 如果一个变量实现没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定的变量。
  8. 对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存(执行 store write 操作)。
  • volatile 型变量的特殊规则
  1. 1) Java 内存模型对 volatile 专门定义了一些特殊的访问规则,当一个变量被定义成 volatile 之后,他将具备两种特性
  2. a) 保证此变量对所有线程的可见性。需要注意 volatile 变量的写操作除了对它本身的读操作可见外,volatile 写操作之前的所有共享变量均对 volatile 读操作之后的操作可见
  3. b) 禁止指令重排序优化。普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获得正确的结果,而不能保证变量赋值操作的顺序与程序中的执行顺序一致,在单线程中,我们是无法感知这一点的
  4. 2) 补充:Java 语言规范规定了 JVM 线程内部维持顺序化语义
  5. 也就是说只要程序的最终结果等同于它在严格的顺序化环境下的结果,那么指令的执行顺序就可能与代码的顺序不一致,这个过程通过叫做指令的重排序。指令重排序存在的意义在于:JVM 能够根据处理器的特性(CPU 的多级缓存系统、多核处理器等)适当的重新排序机器指令,使机器指令更符合 CPU 的执行特点,最大限度的发挥机器的性能。在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整
  • final 域
  1. final 类型的域是不能修改的,除了这一点外,在 Java 内存模型中,final 域还有着特殊的语义. final 域能确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无须同步
  2. 具体而言,就是被 final 修饰的字段在构造器中一旦被初始化完成,并且构造器 <没有把 this 的引用 > 传递出去(this 引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那么在其他线程中就能看到 final 字段的值,而且其外、外部可见状态永远也不会改变。它所带来的安全性是最简单最纯粹的。
  • long 和 double 型变量的特殊规则
  1. Java 内存模型要求 lockunlockreadloadassignusestore write 8 个操作都具有原子性
  2. 但是对于 64 位的数据类型 long double,在模型中特别定义了一条宽松的规定: 允许虚拟机将没有被 volatile 修饰的 64 位数据的读写操作划分为两次 32 位的操作来进行
  3. 这样,如果有多个线程共享一个未被声明为 volatile long double 类型的变量,并且同时对它们进行读取和修改操作,那么某些线程可能会读到一个既非原值,也非其他线程修改值得代表了“半个变量”的数值.
  4. 不过这种读取到“半个变量”的情况非常罕见,因为 Java 内存模型虽然允许虚拟机不把 long double 变量的读写实现成原子操作,但允许迅疾选择把这些操作实现为具有原子性的操作,而且还“强烈建议”虚拟机这样实现。目前各种平台下的商用虚拟机几乎都选择把 64 位数据的读写操作作为原子操作来对待,因此在编码时,不需要将 long double 变量专门声明为 volatile
  • 借助同步

2. 发布

  • 不安全的发布
  • 安全的发布
  • 安全初始化模式
  • 双重检查锁

3. 初始化过程中的安全性