@TOC# Java并发系列
记录在程序走的每一步___auth:huf
Java并发系列开始写作了; 热门框架系列也会持续更新;
并发系列需要一定的计算机硬件基础; 也需要一定的Java基础; 如果连进程跟线程都不了解的情况下; 不建议直接学习并发;学习需要循环递进;
一张图解释 并发与并行
我们知道了什么是并发; 那么我们就开始说一下并发的特性;
创建一个Demo 复制黏贴即可使用
package com.huf;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
*
* auth:huf
*/
public class ThreadTest {
//使用包装类型 Integer 可以保证其可见性 因为里面用了 final
private Integer count = 0;
// 关键字上加 volatile 可以保证其可见性
private Boolean flag = true;
public void refresh() {
flag = false;
System.out.println(Thread.currentThread().getName() + "修改flag:"+flag);
}
public void load() {
while (flag){
count++;
//ThreadTest.getUnsafe().storeFence();//storeFence 可以保证其可见性
//synchronized(this){}//可以 保证其可见性
//Thread.yield();//上下文切换 可以保证其可见性
//System.out.print("");//很神奇吧? print可以保证可见性
}
System.out.println(Thread.currentThread().getName()+"跳出循环:"+count);
}
public static void main(String[] args) throws InterruptedException {
ThreadTest t = new ThreadTest();
new Thread(()->t.load(), "threadA").start();
Thread.sleep(1000);
new Thread(()->t.refresh(),"threadb").start();
}
public static Unsafe getUnsafe(){
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
return (Unsafe) field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
并发有三大特性 :
我个人是这么理解的 (并发的三大问题)【有序性】【可见性】【原子性】 本章节主要讲解线程可见性
可见性 :如何保证其可见性?
什么是可见性?
当一个线程修改了共享变量的值,其他线程能够看到修改的值。Java 内存模型是通过在变量 修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介 的方法来实现可见性的。
我们先讲下内存模型 加深理解一下可见性
JMM 内存模型
什么是JMM :Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM),用于屏蔽掉各 种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效 果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可 以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。JMM 描述的是一种抽象的概念,一组规则,通过这组规则控制程序中各个变量在共享数据区域和私 有数据区域的访问方式,JMM是围绕原子性、有序性、可见性展开的。
我把上面那个程序demo做成图;
我们基于上面那个图 的顺序:
主要场景:
我们主线程 开启《线程一》执行 load() 那么 while (flag) 就会一直循环;
《线程一》会一直从本地内存中读取flag
此时我们开启 《线程二》再执行refresh() ,改变了变量flag 变量flag刷进去主内存
...(后面有很多种场景)
第一个种情况:我们什么都不做, 那么《线程一》就会一直执行下去;
因为两条线程是互不干预的; 《线程一》 一直从自己的本地内存里面读取flag
即使《线程二》改变了flag的值 并且刷新到了主内存中。 《线程一》也是看不见的;
这种情况叫做---》 不可见;
(我们主要是为了解决这个问题; 以下是解决方案)
我们主要要把本地内存 的数据 让它被淘汰 或者 让 线程在本地内存中读取不到数据;
那么就可以保证数据的一致性;
把int 变成 Integer
我们点开源码 即可发现;
final 保证其可见性,一下是执行结果:
变量上加上关键字 volatile
volatile为什么会保证其可见性? 注 : volatile还有其他作用;在其他文章还会继续阐述
volatile:
在汇编成层面上:
volatile在内部实际上使用lock前缀指令;【lock指令不是内存屏障;但是可以达到内存屏障的效果;】
注 : volatile还有其他作用; 在其他文章还会继续阐述 volatile; 实际上在不同层面上volatile 有不同的解释;
在JVM层面上:
volatile 实际上是通过 storeload 调起了Fence() 内存屏障
我们既然知道了 storeload 那么 他们方法一共有四种
也比较有趣 : storestore,storeload,loadload,loadstore
- 在每个volatile写操作的前面插入一个StoreStore屏障
- 在每个volatile写操作的后面插入一个StoreLoad屏障
- 在每个volatile读操作的后面插入一个LoadLoad屏障
- 在每个volatile读操作的后面插入一个LoadStore屏障
使用storeFence
storeFence:调起内存屏障 直接让本地内存失效;
使用 synchronized
synchronized :
底层实际上也是调用storeFence 拉起内存屏障去保证变量的可见性
Thread.yield()
Thread.yield():
上下文切换 以下图为例 我们cpu进行切换的时候 会把当前线程的所有状态进行保存; 包括当前运行到第几行代码等等; 这样下次可以直接到CPU切换回来的时候 回去总线找 上次执行的线程上下文数据地址;然后去内存地址中把数据load回来 然后继续往下执行;
Registers: 就是寄存器
Cache: 当前CPU缓存
ALU: 算法执行器(可以支持多个种基本算术和按位逻辑函数)
这里总结一句话:一旦当前线程 上下文切换了 下一次 我们demo的 flag 就会从内存中读取; 这样就可以保证有序性
题外话: 我们的print 实际上就是内部 用了synchronized 保证了线程的可见性;
我们总结一波;
总结
如何保证多线程的可见性:
1:使用 volatile
2: synchronized关键字
3:使用storeFence;
4: Thread.yield()
… (有非常多的方式)
往底层再次总结: