JMM内存模型与线程
https://blog.csdn.net/javazejian/article/details/72772461
Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(这里的变量包括的是实例字段,静态字段和构成数组对象的元素)的访问方式。也可以屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致性的内存访问效果。
- JVM运行程序的实体是线程,每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据
- Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问
- 但线程对变量的操作(读取赋值等)必须在工作内存中进行
- 首先要将变量从主内存拷贝的自己的工作内存空间,
- 然后对变量进行操作,
- 操作完成后再将变量写回主内存,不能直接操作主内存中的变量,
- 工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成
为了解决cpu与内存的速度差异,引进了缓存一致性。
缓存一致性
每个处理器都有自己的高速缓存,而他们又共享同一主存,(共享主存多核系统)。
问题:当多个处理器运算任务都涉及同一个主存区域的时候,可能导致各自缓存的数据不一致。
为了解决不一致问题,各个处理器访问缓存都需要遵循一些协议。
内存模型:在特定的操作协议下,对特定内存或高速缓存进行读写访问的过程抽象。
乱序优化:除了高速缓存以外,为了使处理器内部的运算单元尽量被充分利用,处理器还可能对代码进行乱序执行,处理器会在计算之后将乱序执行的结果重组,保证结果与顺序执行的结果是一致的,但不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。
Java内存模型
Java内存模型:屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致性的内存访问效果。
主内存 + 工作内存
Java内存模型的主要目的是:定义程序中各种变量的访问规则, 关注变量值存储到内存和从内存中取出变量值这样的底层细节。
- 注意,变量包括实例字段,静态字段和构成数组对象的元素,但是不包括局部变量和方法参数,因为后者是线程私有的。
主内存
- 规定所有的变量都存储在主内存中。
- 注意此处的变量:与java编程的变量有所区别
- 此处主内存的变量包括:实例字段、静态字段、构成数组对象的元素
- 但是不包括局部变量和方法参数,因为后者是线程私有的。
- 注意此处的变量:与java编程的变量有所区别
工作内存
- 每条线程还有自己的工作内存,保存了**该线程使用的变量的主内存副本**
- 线程对变量的所有操作都必须在工作内存中进行。
- 不同的线程之间也不能访问对方线程的变量
- 线程间变量值的传递均需要使用主内存来完成。
了解一下主内存与工作内存的数据存储类型以及操作方式,根据虚拟机规范,
- 对于一个实例对象中的成员方法而言,
- 如果方法中包含本地变量是基本数据类型(boolean,byte,short,char,int,long,float,double),将直接存储在工作内存的帧栈结构中
- 但倘若本地变量是引用类型,那么该变量的引用会存储在工作内存的帧栈中,而对象实例将存储在主内存(共享数据区域,堆)中。
- 对于实例对象的成员变量,不管它是基本数据类型或者包装类型(Integer、Double等)还是引用类型,都会被存储到堆区,因为这个对象就是存储在堆中的。
- static变量以及类本身相关信息将会存储在主内存中。
- 需要注意的是,在主内存中的实例对象可以被多线程共享,倘若两个线程同时调用了同一个对象的同一个方法,那么两条线程会将要操作的数据拷贝一份到自己的工作内存中,执行完成操作后才刷新到主内存。
主内存、工作内存 VS 堆、栈、方法区
- 两个不是一个层次对内存的划分,基本是没有任何关系
- 勉强对应的话
- 主内存对应java堆中的对象实例数据部分
- 工作区对应虚拟机栈中的部分区域
内存间的相互操作
一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存**,Java定义了8个操作:
- lock(锁定):作用于主内存的变量,它把一个变量标识为**一条线程独占**的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
- write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
read+load and store+write** 必须按顺序执行,但是不要求是连续执行。
- 一个新变量只能从主内存中诞生。
- 对一个变量执行lock,会清空工作内存中此变量的值。
- 对一个变量执行unlock之前,必须把此变量同步会内存。
Volatile规则
保证此变量对所有线程的可见性
- 可见性是指:一个线程修改了这个变量的值,新值对于其他线程来说可以立即得知。
误解:volatile变量在各个线程中是一致的(正确),所以基于volatile变量的运算在并发情况下是线程安全的(错误)
- java里面的运算操作并非原子操作,这就导致volatile变量运算在并发时候一样不安全。(volatile不保证原子性) ```java public class ProduceFunction { static volatile int race = 0;
static void increase() { race++; }
public static void main(String[] args) { Thread[] threads = new Thread[20]; for (int i = 0; i < 20; i++) {
threads[i] = new Thread(() -> {
for (int i1 = 0; i1 < 1000; i1++) {
increase();
}
});
threads[i].start();
} System.out.println(race); // 如果线程安全的话,应该是20000 实际是:“17183” } } ```
结论:当getstatic指令把race取到栈顶,volatile保证了race值此时是正确的,但是执行iconst_1、iadd的时候,其他线程可能已经把race改变了,而操作栈顶的值就变成了过期的数据,所以putstatic指令执行后就把旧的race同步到了主内存之中。
volatile可使用的场景:
- 运算结果并不依赖变量的当前值(比如上面程序就依赖变量的当前值),或者可以确保只有单一线程修改变量的值
- 变量不需要与其他的状态变量共同参与不变约束
public class ProduceFunction {
volatile boolean shutdownRequest;
void shutdown() {
shutdownRequest = !shutdownRequest;
}
void doWorl() {
while (!shutdownRequest) {
// 具体代码
}
}
}
第二个作用:禁止指令重排序优化
- 禁止指令重排序优化
普通变量仅能保证方法执行过程中所有依赖赋值结果的地方都能获得正确的结果,但是不保证变量赋值操作的顺序与代码中的执行顺序一致。
可能指令重排导致initialized=true在前面执行,这样线程B可能错误。
代码1:
/**
* Created by zejian on 2017/6/11.
* Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
*/
public class DoubleCheckLock {
private static DoubleCheckLock instance;
private DoubleCheckLock(){}
public static DoubleCheckLock getInstance(){
//第一次检测
if (instance==null){
//同步
synchronized (DoubleCheckLock.class){
if (instance == null){
//多线程环境下可能会出现问题的地方
instance = new DoubleCheckLock();
}
}
}
return instance;
}
}
https://juejin.cn/post/6844904160429621255
这段代码在单线程环境下并没有什么问题,但如果在多线程环境下就可以出现线程安全问题。
原因在于:某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。因为instance = new DoubleCheckLock();
可以分为以下3步完成(伪代码)
memory = allocate(); //1.分配对象内存空间
instance(memory); //2.初始化对象
instance = memory; //3.设置instance指向刚分配的内存地址,此时instance!=null
指令1:获取singleton对象的内存地址
指令2:初始化singleton对象
指令3:将这块内存地址,指向引用变量singleton。
由于步骤1和步骤2间可能会重排序,如下:
memory = allocate(); //1.分配对象内存空间
instance = memory; //3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory); //2.初始化对象
步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成(如果没有Volatile关键字,假设线程A正常创建一个实例,那么指定执行的顺序可能2-1-3,当执行到指令1的时候A时间片耗尽,线程B执行getInstance方法,获取到的,可能是对象的一部分,或者是不正确的对象,程序可能就会报异常信息),也就造成了线程安全问题。
那么该如何解决呢,很简单,我们使用volatile禁止instance变量被执行指令重排优化即可。
//禁止指令重排优化
private volatile static DoubleCheckLock instance;
第三个作用:内存屏障
继续分析代码1:
lock就相当于增加了一个内存屏障,指的是重排序的时候不能把后面的指令重排序到内存屏障之前。
功能:
- synchronized 靠操作系统内核互斥锁实现的,相当于 JMM 中的 lock 和 unlock。退出代码块时一定会刷新变量回主内存
- volatile 靠插入内存屏障指令防止其后面的指令跑到它前面去了
作者:MuziBlogs
链接:https://www.jianshu.com/p/a2d5426a3b40
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
原子性+可见性+有序性
原子性
直接保证:read/load/use/assign/store/write
可以认为:基本数据类型的访问读写都是具备原子性的
可见性
就是一个线程修改了共享变量的值的时候,其他线程可以立刻得知这个修改。
volatile以及普通变量都是变量修改以后将值同步到主内存,变量读取前从主内存刷新变量值,即依赖主内存作为传递媒介。
区别在于volatile可以保证新值立即同步到主内存**,每次使用时候立即从主内存刷新。
有序性
- 如果在本线程内观察,所有操作都是有序的
- 指的是线程内似表现为串行语义
- 如果在一个线程观察另一个线程,所有操作都是无序的
- 指的是指令重排现象和工作内存与主内序的延迟
java提供了synchronized + volatile 保证线程间操作的有序性。
先行发生原则
- 定义两项操作之间的偏序关系
- 操作A先行发生与操作B,也就是操作B发生之前,操作A产生的影响可以被B观察到。
规则
- 程序次序规则:一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
- 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock。
- volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个操字的读操作。
- 线程启动规则:Thread对象的start先行发生于该线程的每一个动作。
- 线程终止规则:线程的所有操作都先行发生于此线程的终止操作。
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
- 对象终结规则:一个对象的初始化先行发生于它的finalize方法
- 传递性:A先行发生于B,B先行发生于C,则A先行发生于C。 ```java private int value = 0;
public void setValue(int value) { this.value = value; }
public int getValue() { return this.value; }
假定有线程1和线程2,线程1先(时间上先)调用"setValue(1)",然后线程2调用"getValue()",最后线程2得到的值是多少?<br />我们根据前面提到的8个规则来分析下:
- 因为两个方法分别在线程1和线程2中执行,不是同一个线程,因此程序次序规则不适用
- 代码中没有任何同步代码块(锁),因此锁规则也不适用
- 代码中value变量为非volatile变量,因此volatile变量规则也不适用
- 整个执行过程很明显和线程启动、终止、中断、对象终结规则也没有关系
- 因为没有任何先行发生关系,所以传递性规则也不适用
根据上面的分析,虽然线程1在操作**时间**上先于线程2,但是因为**没有任何先行发生关系,**所以无法确定线程2中"getValue()"的值,因此这两个线程的操作放在一起是不安全的。<br />要解决这个问题也很简单,一种是将setValue和getValue两个方法都定义为synchronized方法,这样就可以套用锁规则,**另外一种是将value变量定义为volatile变量,而且这里****修改value值的时候不依赖value的原值****,所以就可以套用volatile变量规则。**<br />通过上面的分析,我们可以知道一个操作“时间上的先发生”不代表这个操作会“先行发生”。<br />那一个操作“先行发生”是不是“时间上也是先发生”呢,这个其实也是不能保证的,典型的例子就是指令重排,例如下面的例子:
```java
//下面两个操作在同一个线程中执行
int i = 1; //操作1
int j = 2; //操作2
按照线程次序规则,操作1先行发生于操作2,但是操作2在实际执行过程中很可能因为重排序而被处理器先执行,这样也没有影响先行发生原则的正确性,因为在这个线程中我们无法感知到这种变化。
时间上的先后顺序与先行发生原则之间没有基本的关系,因此我们在衡量线程安全与否时不要关注时间顺序,而是**应该关注先行发生原则**。
**
Java 与线程
线程
- 线程的引入,可以把资源的分配和执行调度分开,各个线程既可以共享进程资源,又可以独立调度
- 线程是java进行处理器资源调度的最基本单位。
线程实现的三种方式
内核线程1:1
用户线程1:N
用户线程+轻量级进程混合实现N:M
- 内核线程实现 (Java实现方式)
- 内核线程Kernel-Level Thread (KLT)直接是由操作系统内核支持的线程,由内核来完成切换。内核通过操作调度器对线程进行切换,并将线程的任务映射到各个处理器上。
- 每个内核线程可以视作内核的一个分身,这样操作系统就有能力同时处理多件事情
- 程序一般使用内核线程的高级接口:轻量级进程Light Weight Process LWP ,也就是通常意义上的线程,每个LWP都有一个内核线程支持,
每个LWP都成为一个独立的调度单元,即使其中一个被阻塞了,也不会影响整个进程的工作。
但是
- 各种线程操作需要系统调用,代价高。
- 1:1,所以线程需要内核线程的支持,所以需要消耗一定的内核资源,所以一个系统支持的线程数量是有限的。
线程调度
- 协同式
- 线程执行时间由线程本身来控制,线程自己执行完以后主动通知另一个线程上去执行
- 好处
- 实现简单
- 缺点
- 线程执行时间不可控制,程序可能一直阻塞
- 好处
- 线程执行时间由线程本身来控制,线程自己执行完以后主动通知另一个线程上去执行
- 抢占式(Java使用的)
- 每个线程由系统分配时间执行
- 新建(NEW)
- 创建后尚未启动的线程处于这个状态
- 这个时候从本质上仅是一个Thread对象。
- 运行(RUNNABLE)
Runnable包括了操作系统线程状态中的Running和Ready,也就是
- 处于此状态的线程可能正在运行,
- 也有可能正在等待CPU为它分配执行时间,就绪态。
- 无限期等待(Waiting)
处于这种状态的线程不会被分配CPU执行时间,它们要等待被其它线程显示地唤醒。
以下方法会让线程陷入无限期等待状态:
1)没有设置timeout参数的Object.wait()方法。
2)没有设置timeout参数的Thread.join()方法。
3)LockSupport.park()方法。
- 限期等待(Timed Waiting)
处于这种状态的线程也不会被分配CPU执行时间,不过无需等待被其它线程显示地唤醒,在一定时间之后它们会由系统自动的唤醒。
以下方法会让线程进入TIMED_WAITING限期等待状态:
1)Thread.sleep()方法
2)设置了timeout参数的Object.wait()方法
3)设置了timeout参数的Thread.join()方法
4)LockSupport.parkNanos()方法
5)LockSupport.parkUntil()方法
- 阻塞(Blocked)
线程被阻塞了
- 结束(Terminated)
已终止线程
阻塞和等待的区别
阻塞状态:在等待着获取到一个排他锁,这个事件在另外一个线程放弃这个锁的时候发生
等待状态:在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。