高效并发
本章主要内容:
- 了解并发问题中的可见性、有序性和原子性问题
- 了解JAVA内存模型以及如何解决并发问题
- 了解先行发生原则
- 了解线程在JAVA中的实现
硬件的效率和一致性
物理计算机中的并发问题是什么?
- 可见性问题:本质上是缓存一致性问题,
- 有序性问题:指令乱序执行优化和指令重排序问题:为了优化指令执行效率,处理器会对指令进行乱序执行处理(单线程中指令重排后的结果和顺序执行一样,所以主要是针对多线程情况,也就是“在一个线程中观察本线程,一切都是有序的,在一个线程中观察另一个线程,所有的操作都是无序的”)
- 原子性问题:任务调度导致查询和修改分离,修改时已经不是真实的数值。
重排序具体会导致什么问题?
1 | public class RecordExample2 { |
上述案例中第1步和第2步的顺序会影响第4步结果
什么是内存屏障?
内存屏障是被插入两个CPU指令之间的一种指令,用来禁止处理器把内存屏障后面的指令重排序从排序到前面(像屏障一样),从而保障有序性的。另外,为了达到屏障的效果,它也会使处理器写入、读取值之前,将写缓冲器的值写入高速缓存,清空无效队列(数据到了高速缓存之后,缓存一致性协议就可以生效了),从而“附带”的保障了可见性。
JAVA内存模型
建立java内存模型是为了解决什么问题?
不同的处理器架构有着不同的内存模塑,因此这些处理器对有序性的保障程度各异,这表现为它们所支持的内存重排序不同(读读,读写,写读,写写)。
Java 作为一个跨平台( 跨操作系统和硬件)的语言,为了屏蔽不同处理器的内存模型差异,以便Java应用开发人员不必根据不同的处理器编写不同的代码,它必须定义自己的内存模型,这个模型就被称为Java内存模型(JMM)。
JMM还可以由以下几点概括:
- JMM是Java的虚拟机规范,是一种
抽象
的概念 - 目的是保证多线程读写共享变量时的安全问题(提供Java关键字,内存屏障是实现volatile、synchronized、final等关键字的底层原理)
- 同时在各种平台架构下都能达到一致的内存访问效果
为了解决并发问题,JMM保证线程安全是围绕原子性、可见性、有序性这3个特性来建立的,缺一不可。
JMM和JVM的区别?
JMM是规范,JVM是实现
JMM的内容?
JMM规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
此时的变量不同于Java编程中的变量,它包括了实例字段、静态字段、构成数组对象的元素,不包括局部变量与方法参数,因为它们是线程私有的,不会被共享,JMM不关心非共享数据。
和JAVA内存区域有何关系?
主内存和工作内存与JVM内存结构中的Java堆、栈、方法区等并不是同一个层次的内存划分,无法直接类比。《深入理解Java虚拟机》中认为,如果一定要勉强对应起来的话,从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分。工作内存则对应于虚拟机栈中的部分区域。
主内存和工作内存的交互协议?
JMM定义了八大原子操作(针对double、long有特殊情况)
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
- write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
规定了在执行上述八种基本操作时,必须满足如下规则:
- 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
- 不允许read和load、store和write操作之一单独出现
- 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
- 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
- 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
- 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
上面八个操作+规则+volatile就可以正确解决并发问题
volatile的作用?
- 保障可见性:
- 写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。
- 保证有序性:类似内存屏障的功能
- 不保证原子性:如i++
volatile底层实现?
volatile变量instance赋值的地方,增加了一条“lock add dword ptr [rsp],0h”指令(汇编)
该指令相当于一个内存屏障
java内存模型如何解决原子性问题?
- synchronized:底层是对lock和unlock操作的运用。
java内存模型如何解决可见性问题?
- volatile:本身语义
- synchronized:底层是对lock和unlock操作的运用。同步块的可见性是由“如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值”、“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”这两条规则获得的。
- final:final关键字的可见性是指,被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那么在其他线程就能看见final字段的值(无须同步)
java内存模型如何解决有序性问题?
- volatile:本身语义
- synchronized:底层是对lock和unlock操作的运用,由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入
什么是“先行发生原则”?有什么意义?
先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到。
先行发生原则
的意义简单的理解就是说:代替JMM规范,忽略底层指令真实执行过程,给程序员一个直观的参考,程序的执行顺序和happens-before原则规定的顺序一致!
,在有并发问题时正确编写安全的代码。
happens-before与时间关系?
两个操作之间具有 happens-before 关系,并不意味着前一个操作必须要在后一个操作之前执行,它和时间没什么关系。
happens-before规则:
- 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
- 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
- volatile的happen-before原则:对一个volatile变量的写操作happen-before后续(指时间的后续)对此变量的任意操作。
- happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
- 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
- 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
- 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
- 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。
JAVA与线程
线程中的方法大都是Native方法,一个Native方法往往意味着使用平台相关的代码来实现
线程模型有哪些?JAVA如何实现线程?
实现线程主要有 3 种方式:使用内核线程实现、使用用户线程实现、使用用户线程加轻量级进程混合实现。
Java 线程在 JDK 1.2 之前是基于称为「绿色线程」的用户线程实现的。而在 JDK 1.2 中,线程模型替换为基于操作系统原生线程模型来实现。
线程调度的方式?
主要调度方式有两种,分别是协同式线程调度和抢占式线程调度:
- 协同式线程调度:线程的执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。
- 优势:协同式多线程的最大好处是实现简单,而且由于线程要把自己的事情做完后才会进行线程切换,切换操作对线程自己是可知的,所有没有线程同步的问题。
- 劣势:线程执行时间不可控,甚至如果一个线程编写有问题,一直不告诉操作系统进行线程切换,那么程序就会一直阻塞在那里。
- 抢占式线程调度:那么每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定。在这种实现线程调度的方式下,线程的执行实现是系统可控的,也不会有一个线程导致整个进程阻塞的问题。
Java使用的线程调度方式就是抢占式的。
如何给一个线程分配更多的执行时间?
给线程设置优先级,有两点需要注意:
- 这里的优先只是建议,并不一定先发生
- JAVA线程优先级和概念和操作系统线程优先级的概念并不一一对应
线程状态有什么意义?
- 新建(New):创建后尚未启动的线程处于这种状态;
- 运行(Runnable):Runnable 包括了操作系统线程状态中的 Running 和 Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着 CPU 为它分配执行时间;
- 无限期等待(Waiting):处于这种状态的线程不会被分配 CPU 执行时间,它们要等待被其它线程显式地唤醒;以下三种方法会让线程进入无限期等待状态:
- 没有设置 TimeOut 参数的 Object.wait();
- 没有设置 TimeOut 参数的 Thread.join();
- LockSupport.park()。
- 限期等待(Timed Waiting):处于这种状态的线程也不会被分配 CPU 执行时间,不过无需等待被其它线程显式地唤醒,在一定时间之后它们会由系统自动唤醒;以下方法会让线程进入限期等待状态:
- Thread.sleep();
- 设置了 TimeOut 参数的 Object.wait();
- 设置了 TimeOut 参数的 Thread.join();
- LockSupport.parkNanos();
- LockSupport.parkUntil()。
- 阻塞(Blocked):线程被阻塞了,「阻塞状态」和「等待状态」的区别是:「阻塞状态」在等待着获取一个排他锁,这个事件将在另一个线程放弃这个锁的时候发生;而「等待状态」则是在等待一段时间,或者唤醒动作的发送。在程序等待进入同步区域时,线程将进入这种状态;
- 结束(Terminated):线程已经结束执行。