JMM(java memory model)java内存模型,它并没有实际的体现,它是一个规则,都知道ava是跨平台语言,在个操作系统中内存都有一定的差异性,每个系统的并发不一致,JMM的作用就是用来屏蔽掉不同操作系统中的内存差异性来保持并发的一致性。同时JMM也规范了JVM如何与计算机内存进行交互。JMM就是Java自己的一套协议来屏蔽掉各种硬件和操作系统的内存访问差异,实现平台一致性达到最终的"一次编写,到处运行"
上面的抽象图可以看到共享变量是在主内存里,但是在修改的时候线程会将变量拷贝到自己的工作空间内,修改后再刷回住内存的这样一个概念,中间线程操作变量是由JMM控制的,下面看一下它的规则。
上面说了线程是将变量拿到自己的工作空间进行修改后再写会到主内存,如果线程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、正确的编程方式,只有确认上面三步都执行完成后再去返回对象
上面所说的步骤其实就是实现了线程之间的通信,但是不要以为线程之间的通信就是这么简单的,其实在Java中JMM内存模型定义了八种操作来实现同步的细节。
所以看似简单的通信其实是这八种状态来实现的。
同时在Java内存模型中明确规定了要执行这些操作需要满足以下规则:
所以上面说的操作要严格执行。