前言
这几天看了几章《java并发编程的艺术》,及时总结,别忘的太快。
知识点
1、如何减少上下文切换
- 无锁并发编程:通过无锁设计来避免锁的使用,比如让不同的线程处理不同段的数据。
- CAS:CAS即Compare And Set,通过CAS算法来更新数据,保证数据更新的安全性。
- 使用最小线程:在使用线程池时,尽量做到大池小队列(IO密集型)以及小池无队列(CPU密集型),后者使用小池可以避免线程上下文的来回切换,在CPU密集型任务的调度中,切换的代价甚至可能高于其任务本身。
- 协程:在单线程中实现多任务的调度。
2、volatile的两条实现原则
- Lock前缀指令会引起处理器缓存回写到缓存:其实现原理主要是通过锁总线或者锁缓存(一般都是锁缓存,锁总线开销太大,意味着其他处理器都不能访问系统内存),锁缓存即锁定这块内存区域的缓存,并回写到内存,并使用缓存一致性机制来确保修改的原子性。
- 缓存失效:一个处理器的缓存回写到内存会导致其他处理器的缓存失效,这样便能保证各个处理器在需要该数据时无法在处理器缓存直接获取,而需要从内存重新读取,保证数据的一致性。
3、锁的三种表现形式
- 对于同步方法,锁是当前实例对象。
- 对于静态同步对象,锁是当前类的Class对象。
- 对于同步方法块,锁是Synchonized括号配置的对象。
4、对象头的组成
- Mark Word : 存储对象的hashcode或锁信息等。(1字宽)
- Class Metadata Address : 存储到对象类型数据的指针(1字宽)。
- Array length :如果当前对象是数组,则为数组的长度(非数组类型没有)(32bit)。
5、锁的4种状态对比以及升级
- 无锁:级别最低的状态。
- 偏向锁:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出的同步块时则不需要使用CAS来加锁和解锁。大致流程如下:线程在访问某个对象时,首先检查该对象头里是否存储本线程ID,如果,有,则直接操作,如果没有,再检查下Mark Word中的偏向锁标示是否为1;如果还是没有,则使用CAS竞争锁,如果标示为1,则将该线程ID放到对象头中。偏向锁使用等到竞争出现才释放锁的机制,其撤销需要等待全局安全点,即没有正在执行的字节码时才会撤销。通过-XX:BiasedLocking=false设置可以关闭偏向锁,那么程序会默认进入到轻量级锁状态。
- 轻量级锁:加锁过程:线程在获取锁时,首先会在将对象头中的Mark Word复制到线程中用于存储锁记录的栈帧中(Displaced Mark Word),并尝试将Mark Word替换为指向锁记录的指针,如果成功,则获取到锁,如果失败,则采用自旋的方式来获取锁。解锁过程:使用CAS操作将Displaced Mark Word替换回对象头,如果成功,表示没有竞争,如果失败,则升级为重量级锁。
- 重量级锁:竞争重量级锁的线程不会使用自旋,因此也不会消耗CPU,同时,在等待锁释放的过程中,便会阻塞,适用于同步快执行时间较长的场景,这样可以提高系统吞吐量。
6、原子操作的实现原理
- 总线锁:提供一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
- 缓存锁定:在锁定缓存期间,阻止其他处理器修改处理器缓存的内存区域,当处理器回写已被锁定的缓存行数据时,会使缓存行无效,其实就是通过缓存失效来保证数据更新的原子性。
7、java CAS操作实现原子操作的三大问题
- ABA问题:一个值由A变为B,然后又变为A,使用CAS会发现值没有变化。解决思路:在变量前追加版本号。
- 循环时间长开销大:长时间执行不成功,会给带来巨大的CPU开销。解决方案:使用pause指令。
- 只能保证一个共享变量的原子操作:对多个共享变量操作时,无法保证操作的原子性,比如两个共享变量相加,如果两个值都变了,但是它们的合没变。解决方案:把多个共享变量合成一个共享变量。
8、重排序的三种类型
- 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以安排语句的执行顺序。
- 指令级并行的重排序:采用指令级并行技术来执行多条指令的重叠执行,前提是不存在数据依赖性。
- 内存系统的重排序:使用读写缓冲区,使得加载和读取操作看起来可能是在乱序执行。
9、4种内存屏障类型
- LoadLoad屏障:抽象场景:Load1; LoadLoad; Load2;Load1 和 Load2 代表两条读取指令。在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
- StoreStore屏障:抽象场景:Store1; StoreStore; Store2Store1 和 Store2代表两条写入指令。在Store2写入执行前,保证Store1的写入操作对其它处理器可见。
- LoadStore屏障:抽象场景:Load1; LoadStore; Store2;在Store2被写入前,保证Load1要读取的数据被读取完毕。
- StoreLoad屏障:抽象场景:Store1; StoreLoad; Load2;在Load2读取操作执行前,保证Store1的写入对所有处理器可见。StoreLoad屏障的开销是四种屏障中最大的。
10、happens-before规则
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程的任意后续操作。
- 监视器锁规则:对于一个锁的解锁,happens-before与随后对于这个锁的加锁。
- volatile变量规则:对于一个volatile域的写,happens-before与任意后续对这个volatile域的读。
- 传递性:如果A happens-before B,B happens-before C,那么A happens-before C。
11、数据依赖类型表
- 前面描述了数据依赖性对重排序的限制,数据依赖类型包括以下三种:
- 写后读:写一个变量后,再读这个位置。
- 写后写:写一个变量后,再写这个变量。
- 读后写:读一个变量后,再写这个变量。
12、As-if-serial语义
无论怎么重排序,单线程程序的执行结果不能被改变,编译器和处理器都必须遵守这个规则。
13、顺序一致性模型与JMM的差异
- 顺序一致性模型保证单线程内的操作会按程序步骤顺序执行,而JMM不能保证。
- 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不能保证。
- JMM不保证对于64位long和double型变量写操作的原子型,而顺序一致性模型保证对所有的内存读写操作都具有原子性。
14、volatile写-读的内存语义
- 写:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
- 读:当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,后续线程对该变量的访问都将直接到主内存中访问。
15、volatile的内存屏障插入策略
- 在每个volatile写操作前插入StoreStore屏障。
- 在每个volatile写操作后面插入StoreLoad屏障。
- 在每个volatile读操作后面插入LoadLoad屏障。
- 在每个volatile读操作后面插入一个LoadStore屏障。
16、双重检查锁失效的解决方案。
双重检查锁是一种理想的单例工厂实现方案,然而由于重排序机制,可能会导致其失效,即将(1、对象初始化)与(2、设置Instance指向刚分配的内存地址)这两条指令重排序,导致取到instance不为null而引用的对象还没有初始化完成。常用的解决方案包括以下两种:
- 基于volatile的解决方案:将instance声明为volatile类型,这样便能禁止上面1 、 2的重排序。
- 基于类初始化的解决方案:JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会去获取一个锁,可以用于同步多个线程对同一个类的初始化,因此可以通过将instance的实例化过程定义在一个静态内部类中,并定义静态的getInstance方法,即可解决双重检查锁的失效问题。
总结
未完待续!