1:为什么需要JMM
JMM是java memory model(Java内存模型)
如果不存在内存模型的概念,运行的结果依赖于处理器,不同的处理器结果不同,无法保证并发安全。所以需要一个标准,让多程序运行的结果可预期。
JMM是一种规范,需要各个JVM的实现来遵守JMM规范,以便开发者可以利用这些规范,更方便地开发多线程程序。(JVM有多种实现,有oracle,有openjdk的)
重点的3点:重排序、原子性、可见性
2:重排序
1:什么是重排序
第一种情况
jmm/OutOfOrderExecution.java
package jmm;
/**
* 描述:演示重排序的现象 “直到达到某个条件才停止”,去测试小概率事件
*/
public class OutOfOrderExecution {
//这义4个变量
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
//线程one
Thread one = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
x = b;
}
});
//线程two
Thread two = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
y = a;
}
});
one.start();
two.start();
//让主线程等待
one.join();
two.join();
//打印结果
System.out.println("x=" + x + ", y=" + y);
}
}
分析
开始x =0,后来x=b,开始y=0,后来y=a
如果线程1先运行且运行完,a=1,x=b,而线程2还没有运行,赋值b=1还没有去做,b还为0,将b给x,此时x=0
当第2个线程运行,修改y=a,而a已经被第1个线程修改为1,所以y=1
这只是其中一种情况。
这4行代码的顺序会决定最终x和y的结果,一共有3种情况:
1)线程1先运行
a=1;x=b(0);b=1;y=a(1),最终结果是x=0,y=1
2)线程2先运行
b=1;y=a(0);a=1;x=b(1),最终结果是x=1,y=0
3)线程1第一行去赋值b,线程2第一行去赋值a,然后再分别执行两个线程的第2行代码,去赋值x和y
b=1;a=1;x=b(1);y=a(1),最终结果是x=1,y=1
第二种情况
将两个线程换下顺序执行
jmm/OutOfOrderExecution.java
two.start();
one.start();
第三种情况
要求两个线程同时开始,使用一个工具类CountDownLatch(起到闸门的作用)
jmm/OutOfOrderExecution2.java
package jmm;
import java.util.concurrent.CountDownLatch;
/**
* 描述: 演示重排序的现象 “直到达到某个条件才停止”,去测试小概率事件
*/
public class OutOfOrderExecution {
//这义4个变量
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
//初始值表示需要几次倒计时
CountDownLatch latch = new CountDownLatch(1);
//线程one
Thread one = new Thread(new Runnable() {
@Override
public void run() {
//在线程需要等待的地方加上栅栏,当收到发射信号时线程1和线程2同时执行
try {
latch.await();
a = 1;
x = b;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//线程two
Thread two = new Thread(new Runnable() {
@Override
public void run() {
try {
//在线程需要等待的地方加上栅栏,当收到发射信号时线程1和线程2同时执行
latch.await();
b = 1;
y = a;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
two.start();
one.start();
//放开闸门,让线程1和线程2同时运行
latch.countDown();
//让主线程等待
one.join();
two.join();
//打印结果
System.out.println("x=" + x + ", y=" + y);
}
}
如果第3种情况不好测试,可以试试改成下面的代码,来测试小概率事件。
jmm/OutOfOrderExecution2.java
package jmm;
import java.util.concurrent.CountDownLatch;
/**
* 描述: 演示重排序的现象 “直到达到某个条件才停止”,去测试小概率事件
*/
public class OutOfOrderExecution2 {
//这义4个变量
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for (; ; ) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
CountDownLatch latch = new CountDownLatch(3);
//线程one
Thread one = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.countDown();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.countDown();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
b = 1;
y = a;
}
});
two.start();
one.start();
latch.countDown();
one.join();
two.join();
String result = "第" + i + "次(" + x + "," + y + ")";
if (x == 1 && y == 1) {
System.out.println(result);
break;
} else {
System.out.println(result);
}
}
}
}
第四种情况
X=0,y=0
jmm/OutOfOrderExecution2.java
if (x == 0 && y == 0) {
System.out.println(result);
break;
} else {
System.out.println(result);
}
这是因为发生重排序了,4行代码的执行顺序的其中一种可能
y=a; (a开始为0,所以y=0)
a=1;
x=b;
b=1;
在线程1内部的两行代码的实际执行顺序和代码在java文件中的顺序不一致,代码指令并不是严格按照代码语句顺序执行的,它们的顺序被改变了,这就是重排序。这里被颠倒的是y=a和b=1这两行语句。
2:解决重排序
jmm/OutOfOrderExecution.java
//这义4个变量
private volatile static int x = 0, y = 0;
private volatile static int a = 0, b = 0;
3:可见性
1:可见性带来的问题
jmm/FieldVisibility.java
package jmm;
/**
* 描述: 演示可见性带来的问题
*/
public class FieldVisibility {
//类的两个成员变量,现在用两个线程对变量进行操作。
int a = 1;
int b = 2;
private void change() {
a = 3;
b = a;
}
private void print() {
System.out.println("b=" + b+";a=" + a);
}
public static void main(String[] args) {
while (true) {
FieldVisibility test = new FieldVisibility();
//线程1调用change()
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
//线程2调用println()
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
}
分析:
情况1:线程1先启动运行,执行change(),b=3;a=3
情况2:线程2先执行,先执行print(),b=2;a=1
情况3:b=2;a=3是两个线程交替执行,线程1将a=3,然后第2个线程就进行打印
情况4(体现可见性): b=3; a=1,线程1先运行,将a=3,b=3,但是线程1修改后的a=3,b=3并不一定会传达给线程2,线程2可能只看到对b的修改,但没有看到对a的修改,所以b=3,但是线程2没有看到线程1对a的修改,线程2可以看到原始的a=1
由于结要情况4的结果不好找,我们安装一个Grep Console插件
改进
jmm/FieldVisibility.java
volatile int a = 1;
volatile int b = 2;
2:为什么会有可见性问题
这是因为CPU有多级缓存,导致读的数据过期
如果所有核心都只用一个缓存,就没有内存可见性问题。
每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存中。所以会导致有些核心读取的值是一个过期的值。
3:JMM主内存和本地内存
Java作为高级语言,屏蔽了底层的实现细节。用JMM定义了一套读写内存数据的规范,使我们不再需要关心一级缓存、二级缓存、三级缓存这些问题。但是JMM抽象出了主内存和本地内存的概念。
这里说的本地内存并不是真的是一块给每个线程分配的内存,而是JMM的一个抽象,是对寄存器、一级缓存、二级缓存等的抽象。
如图:一个线程(就是一个核心)和自己的工作内存沟通,不同的线程工作内存是不互通的。线程通过buffer和主内存沟通。线程间的交互也只能通过主内存进行。
总结:
1)所有的变量都存储在主内存中,同时每个线程也有自己独立的工作内存,工作内存中的变量内容是主内存中的拷贝。
2)线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后再同步到主内存中。
3)主内存是多个线程共享的,但线程间不共享工作内存。如果线程间需要通信,必须借助主内存中转来完成。
所有的共享变量存在于主内存中,每个线程都有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致可见性问题。
4:Java内存模型中的原子性、有序性、可见性是什么
并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
1:原子性
一个操作或多个操作要么全部执行完且执行过程不被中断,要么就不执行。
2:可见性
当多个线程同时访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
3:有序性
程序执行的顺序按照代码的先后顺序执行。
对于单线程,在执行代码时jvm会进行指令重排序,处理器为了提高效率,可以对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证保存最终执行结果和代码顺序执行的结果是一致的。
5:Volatile和synchronized的关系
volatile可以看成是轻量版的synchronized
如果一个共享变量自始至终只被各个线程赋值,而没有其它操作,则可以用volatile代替synchronized,因为赋值自身是有原子性的,而volatile又保证了可见性,所以可以保证线程安全。
Volatile属性的读写操作是无锁的,但它不能替代synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以它低成本。