Java并发编程实战

Java 内存模型

Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括 volatile、synchronized 和 final 三个关键字,以及六项 Happens-Before 规则。

Happens-Before 规则

前面一个操作的结果对后续操作是可见的
Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则。

  1. 程序的顺序性规则
  2. volatile 变量规则
    对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。
  3. 传递性
  4. 管程中锁的规则
    对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
  5. 线程 start() 规则
    主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
  6. 线程 join() 规则

互斥锁-原子性问题

如果能够保证对共享变量的修改是互斥的,那么,无论是单核 CPU 还是多核 CPU,就都能保证原子性了。
临界区、锁:锁的是什么?我们保护的又是什么?
Java 提供的 synchronized 关键字,就是锁的一种实现。

  • 当修饰静态方法的时候,锁定的是当前类的 Class 对象;
  • 当修饰非静态方法的时候,锁定的是当前实例对象 this。

受保护资源和锁之间合理的关联关系应该是 N:1 的关系。
解决原子性问题,是要保证中间状态对外不可见。
使用细粒度锁可以提高并行度,是性能优化的一个重要手段。
死锁:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。

只有以下这四个条件都发生时才会出现死锁:

  • 互斥,共享资源 X 和 Y 只能被一个线程占用;
  • 占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
  • 不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
  • 循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。

反过来分析,也就是只要破坏其中一个,就可以成功避免死锁的发生
一个完整的等待-通知机制:线程首先获取互斥锁,当线程要求的条件不满足时,释放互斥锁,进入等待状态;当要求的条件满足时,通知等待的线程,重新获取互斥锁。

wait()和 sleep()的区别?

管程,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发.

将硬件的性能发挥到极致

有三个方法 A、B、C,他们的调用关系是 A->B->C(A 调用 B,B 调用 C),在运行时,会构建出下面这样的调用栈。每个方法在调用栈里都有自己的独立空间,称为栈帧,每个栈帧里都有对应方法需要的参数和返回地址。当调用方法时,会创建新的栈帧,并压入调用栈;当方法返回时,对应的栈帧就会被自动弹出。也就是说,栈帧和方法是同生共死的。
调用栈

调用栈与线程

两个线程可以同时用不同的参数调用相同的方法,那调用栈和线程之间是什么关系呢?答案是:每个线程都有自己独立的调用栈。

如何用面向对象思想写好并发程序?

一、封装共享变量:将共享变量作为对象属性封装在内部,对所有公共方法制定并发访问策略。
二、识别共享变量间的约束条件:一定要识别出所有共享变量之间的约束条件,如果约束条件识别不足,很可能导致制定的并发访问策略南辕北辙。共享变量之间的约束条件,反映在代码里,基本上都会有 if 语句,所以,一定要特别注意竞态条件。
三、制定并发访问策略:避免共享、不变模式、管程及其他同步工具。