1. JMM做了什么
JMM(java memory model)java内存模型,它并没有实际的体现,它是一个规则,都知道ava是跨平台语言,在个操作系统中内存都有一定的差异性,每个系统的并发不一致,JMM的作用就是用来屏蔽掉不同操作系统中的内存差异性来保持并发的一致性。同时JMM也规范了JVM如何与计算机内存进行交互。JMM就是Java自己的一套协议来屏蔽掉各种硬件和操作系统的内存访问差异,实现平台一致性达到最终的"一次编写,到处运行"
2. JMM抽象图
上面的抽象图可以看到共享变量是在主内存里,但是在修改的时候线程会将变量拷贝到自己的工作空间内,修改后再刷回住内存的这样一个概念,中间线程操作变量是由JMM控制的,下面看一下它的规则。
3. 规则
可见性
上面说了线程是将变量拿到自己的工作空间进行修改后再写会到主内存,如果线程1刚拿到了变量而线程2把变量进行修改了,线程1不知道而他执行的业务里又使用了变量那它执行的结果就出现了问题,下面代码示例:
public static void main(String[] args) {
// 定义一个内部数据类
class IsLookData{
int i = 0;
// 调用这个方法修改值
public void add10(){
this.i = 10;
}
}
IsLookData isLookData = new IsLookData();
// lambda表达式创建线程
new Thread(()->{
// 休眠1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 加值并写会主内存
isLookData.add10();
// 输出主内存的值查看是否更改成功
System.out.println(Thread.currentThread().getName()+" 的值"+isLookData.i);
}).start();
// 等待变量值修改再执行下一步
while (isLookData.i == 0){
// 循环等待
}
System.out.println(Thread.currentThread().getName()+" 的值"+isLookData.i);
}
执行上面的代码会怎么:会死循环,main线程在启动后拿到的值是0所以会进入while循环等待值不为0,线程里sleep是让它的问题放大,就是保证main线程进入while后变量才发生变化,否则有可能不进入循环就直接验证非0结束了,而线程1修改变量后并没有通知main线程,也就是main线程看不到变量发生了改变所以它会一直死循环。
解决办法,在变量加入volatile,在这个变量发生修改的时候会通知使用这个变量的所有线程重新拿取变量
原子性
原子性的概念都清楚不可分割,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。
代码示例:
class JmmAtomic{
public static void main(String[] args) {
// 定义一个内部数据类
class IsLookData{
volatile int i = 0;
// 调用这个方法修改值
public void add(){
this.i++;
}
}
IsLookData isLookData = new IsLookData();
// 创建50个线程
for (int i = 0; i < 50; i++) {
// lambda表达式创建线程
new Thread(()->{
// 每个线程执行1000次数字累加
for (int j = 0; j < 1000; j++) {
isLookData.add();
}
}).start();
}
// 休眠1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出结果
System.out.println(Thread.currentThread().getName()+" 的值"+isLookData.i);
}
}
执行结果:
可以看到我们50个线程每个加1000个应该是50000个,最终的值却不足50000,我们是使用了volatile的,而volatile是不保证原子性的,所以值出现了差异,怎么解决这样的问题,想到的肯定是加锁synchronized、locks,这样肯定可以解决这个问题,但是我只为了一个数值的变化就加锁,锁可是很影响效率的,所以使用原子类,我们用的int所以使用原子类AtomicInteger找到里面等于i++的方法incrementAndGet,现在再执行看一下结果,代码:
public static void main(String[] args) {
// 定义一个内部数据类
class IsLookData{
volatile int i = 0;
// 原子类
AtomicInteger atomicInteger = new AtomicInteger();
// 调用这个方法修改值
public void add(){
this.i++;
// 等同于i++
atomicInteger.incrementAndGet();
}
}
IsLookData isLookData = new IsLookData();
// 创建50个线程
for (int i = 0; i < 50; i++) {
// lambda表达式创建线程
new Thread(()->{
// 每个线程执行1000次数字累加
for (int j = 0; j < 1000; j++) {
isLookData.add();
}
}).start();
}
// 休眠1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出结果
System.out.println(Thread.currentThread().getName()+" 的值"+isLookData.i);
System.out.println(Thread.currentThread().getName()+" 线程原子类的值"+isLookData.atomicInteger.get());
}
结果:
多输出几次,原子类的值一直是50000,结果正确,原子类的底层使用的是CAS保证的原子性。
有序性
有序性,在我们的机器执行代码的时候他并不是按照我们写的顺序执行的,它会为了优化执行顺序进行指令重排,单线程的情况下是没问题的,保证了单线程的执行,而多线程的情况下因为交替执行很可能因为指令重拍而出现错误。
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = 1 + 5;
c = c * b;
int d = c * a;
}
在上面的代码重排后,a可以是第一个执行的,b也可以是第一个执行的,c也可以是第一个执行的,但是5和6行不可能是第一个执行的,因为它有数据依赖性,c = c*b依赖了c和b所以它要等待c和b有了后再执行,它是没问题的,但是第六行会出现问题吗?
会的,指令重拍后第6行去了第5行,我数据依赖了c和a,(c = 6、a = 1)它俩现在都有所以可以执行,但是c的值不对,少了一部c = c * b; 所以重排后的结果是 6 * 1 = 6 正确结果是 6 * 2 * 1 = 12
怎么解决:
使用volatile修饰方法或变量,它会在执行禁止重拍的代码前后加上内存屏障告诉程序你不要重拍这部分的代码以保证顺序执行,而synchronized是怎么保证有序的,synchronized是直接将方法和块锁定变成单线程方式每次只有一个线程执行里面的内容,如果synchronized是加在方法上是没问题的,如果是加在代码块上是可能有问题的:
// 单例模式
class OneDemo{
private static OneDemo oneDemo = null;
private OneDemo(){
System.out.println("构造方法");
}
//开放一个公有方法,判断是否已经存在实例,有返回,没有新建一个在返回
public static OneDemo getInstance(){
if(oneDemo == null){
synchronized (OneDemo.class){
oneDemo = new OneDemo();
}
}
return oneDemo;
}
}
原因在于某一个线程执行到第一次检测,读取到的oneDemo不为null时,而oneDemo的引用对象没有完成初始化。
oneDemo = new OneDemo(); 这行代码可是说是由下面三步完成的
memory = allocate(); // 1.分配对象内存空间
oneDemo(memory); // 2.初始化对象
oneDemo = memory; // 3.设置oneDemo指向刚分配的内存地址,现在的oneDemo!=null
而在并发环境下其他线程会到if(oneDemo == null)判断再到锁,而在指令重拍后我们的第三行优先执行,但是现在还没有初始化对象,其他线程访问if(oneDemo == null)不为空了就直接返回了对象,但是这个对象并没有初始化就出现了问题。
解决:
1、在方法体上加上volatile让这段代码不被指令重拍
2、正确的编程方式,只有确认上面三步都执行完成后再去返回对象
4. 通信
上面所说的步骤其实就是实现了线程之间的通信,但是不要以为线程之间的通信就是这么简单的,其实在Java中JMM内存模型定义了八种操作来实现同步的细节。
read 读取,作用于主内存把变量从主内存中读取到本本地内存。
load 加载,主要作用本地内存,把从主内存中读取的变量加载到本地内存的变量副本中
use 使用,主要作用本地内存,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。、
assign 赋值 作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store 存储 作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
write 写入 作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
lock 锁定 :作用于主内存的变量,把一个变量标识为一条线程独占状态。
unlock 解锁:作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
所以看似简单的通信其实是这八种状态来实现的。
同时在Java内存模型中明确规定了要执行这些操作需要满足以下规则:
不允许read和load、store和write的操作单独出现。
不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现
如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
所以上面说的操作要严格执行。
作者:安余生大大
链接:https://juejin.cn/post/6989562956677644302
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。