线程的生命状态
为什么用到并发
- 充分利用到多核cpu 的计算能力
- 方便业务的拆分 , 提升应用性能
- 并发的会产生问题
- 高并发的场景下 , 导致频繁的上下文切换
- 临界区的安全问题 , 容易产生死锁
- 线程的上下文切换
- 线程执行时经时间片的轮转 , 当线程A 未执行完毕就开始执行线程B时 , 讲线程的的执行信息保存在Tss任务状态段 ,
- 在时间片的轮转后 , 开始执行线程A , 冲TSS任务段读取线程A的指令, 程序指令、中间数据 到cpu寄存器缓存中 , 开始正式执行线程A
java 内存区域逻辑图
java 内存模型内存交互操作
JMM-同步八种操作介绍(1)
- lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
- (2)unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- (3)read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
- (4)load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
- (5)use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
- (6)assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
- (7)store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
- (8)write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中
如果要把一个变量从主内存中复制到工作内存中,就需要按顺序地执行read和load操作,如果把变量从工作内存中同步到主内存中,就需要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行
同步规则分析1)
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或者assign)的变量。即就是对一个变量实施use和store操作之前,必须先自行assign和load操作。
- 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现。
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行load或assign操作初始化变量的值。
- 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
- 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)
Volatile
voaltiel的内存语义
- 是java虚拟机提供的轻量级的同步机制有一下两个作用
- 保证volatile修饰的共享变量对所有线程可见(当其中的一个线程修改共享变量值 , 新值可以被其他线程立刻得知)
- 禁止指令重新排列优化
volitile 的可见性
- 被volatile修饰的变量对所有线程总数立即可见的,对volatile变量的所有写操作总是能立刻反应到其他线程中
publicclassVolatileVisibilitySample {
volatile boolean initFlag = false;
publicvoidsave(){
this.initFlag = true;
String threadname = Thread.currentThread().getName();
System.out.println("线程:"+threadname+":修改共享变量initFlag");}
public void load(){
String threadname = Thread.currentThread().getName();
while(!initFlag){
//线程在此处空跑,等待initFlag状态改变}System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变");
}
publicstaticvoidmain(String[] args){
VolatileVisibilitySample sample = newVolatileVisibilitySample();
Thread threadA = newThread(()->{sample.save();},"threadA");
Thread threadB = newThread(()->{sample.load();},"threadB");
threadB.start();
try{Thread.sleep(1000);}
catch(InterruptedException e) {
e.printStackTrace();
}threadA.start();
}}
线程A改变initFlag属性之后,线程B马上感知到
- volatile 无法保证原子性
public class Volatile Visibility {
public static volatile inti =0;
public static void increase(){
i++;}
}
在并发的场景下 , i 的变量任何改变会立刻反应到其他线程 , 但当多个线程同时调用increate()时 , 线程出现安全问题
i++ 不具备原子性 , (i 是先取值 ,再写一个回值,两步完成)(会出现的问题):问题 : 若第二个线程于第一个线程 同时read i 的旧值 , 并同时执行 i++ , 会造成线程安全失败
解决 : ,因此对于increase方法必须使用synchronized修饰 , 以此确保线程安全 , 由于synchronized本身也具备与volatile相同的特性 , 所以可以省略volatile 的修饰变量
指令重排
发生在编译器 ,或者是机器码的时候
条件:逻辑上 感知不到指令重排会造成的影响()
- volatile关键字另一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象
实现禁止指令重排于内存屏障
- 内存屏障
- 写内存屏障(Store Memory Barrier):处理器将存储缓存值写回主存(阻塞方式)。
- 读内存屏障(Load Memory Barrier):处理器,处理失效队列(阻塞方式)。
public class DoubleCheckLock {
private static DoubleCheckLock instance;
private DoubleCheckLock(){}
public static DoubleCheckLock getInstance(){
//第一次检测
if(instance==null){//同步
synchronized(DoubleCheckLock.class){
if(instance == null){//多线程环境下可能会出现问题的地方
instance = newDoubleCheckLock();
}}}
returninstance;}}
- 当代码在多线程下执行时可能会产生的问题
在正常的情况下,代码的执行顺序
memory = allocate(); // , 分配内存空间
instance(memory); // 初始化对象
instance = memory; // 设置instance指向的内存地址 , 此时instance != null
可能会导致的情况:重排序
memory = allocate() ; // 1 。 分配空间
instance = memory; // 3. 设置instance 指向的刚分配的地址 , 此刻 , instance 并未初始化
instance(memory) //2. 初始化对象
-
步骤二和步骤三不存在数据依赖的关系 , 重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的 , 但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性
// 禁止指令重排privatevolatilestaticDoubleCheckLock instance