一、概念
很多人将【java内存结构】与【java内存模型分不清】,【java内存模型】是Java Memory Model (JMM)的意思。
简单说 JMM定义了一套在多线程读写共享数据(成员变量、数组)时,对数据的可见性、有序性、和原子性的规则和保障。
二、原子性
java内存模型中通过synchronized关键字来保证原子性,synchronized重量级,也可以保证可见性
语法:
synchronized(对象){
要作为原子操作代码
}
用synchronized可以解决并发操作
三、可见性
先看下面一段代码,main线程对run变量的修改对于t线程不可见,导致t线程无法停止
public class ThreadTest {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
while (true){
}
});
t.start();
Thread.sleep(1000);
run = false;//t线程不会预想停下来
}
}
下面来分析
1.初始状态,t线程刚开始从主内存读取了run的值到工作内存
2.因为t线程要频繁的从主内存读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存中(可理解static变成一个局部的变量),减少对主内存run的访问,提高效率
3.1秒后,main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
解决方案:volatile(易变关键字)
它可以用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主内存中获取它的值,线程操作volatile变量都是直接操作主内存
体现了可见性,保证多个线程之间,一个线程对volatile 变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况
public class ThreadTest {
volatile static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
while (run){
}
});
t.start();
Thread.sleep(1000);
run = false;//t线程会预想停下来
}
}
四、有序性
指令重排理解:
同一线程内,JVM会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码
static int i;
static int j;
//在某个线程内执行如下赋值操作
i =...;//较为耗时的操作
j =...;
可以看到,至于先执行i还是先执行j,对最终结果不产生影响。所以,上面代码真正执行时,既可以是
i =...;//较为耗时的操作
j =...;
也可以是
j =...;
i =...;//较为耗时的操作
这种特性称为指令重排,但多线程下指令重排会影响正确性,例如,著名的double-checked locking模式实现单例
public final class Singleton {
private static Singleton singleton = null;
private Singleton() { }
public static Singleton getSingletonInstance() {
//实例没有创建,才会进入内部的synchronized代码块
if (singleton == null) {
synchronized (Singleton.class) {
//也许其它线程已经创建了实例,所以需要在判断一次
if (singleton != null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
以上的实现特点是:
- 懒惰化
- 首次使用getInstance()才使用synchronized 加锁,后续使用时无需加锁
以上代码看似完美,但在多线程情况下,没有考虑指令重排的情况,
先调构造方法还是先给静态变量赋值,jvm认为谁先执行谁后执行没有影响
加入两个线程,
线程一先进入同步代码块,先执行singleton = new Singleton();分配空间,对Singleton对象生成了引用地址,此时线程2进入
线程2还未进入同步代码块且singleton 不为空,直接返回对象引用,但是现在问题来啦,t线程还未执行完,如果对象的构造过程比较复杂,那么线程2拿到的是一个不完整的对象实例,因为构造方法还未执行完,有的属性赋值完成,有的属性赋值还未完成,使用时可能会出现问题,当然出现这种几率很小
对instance使用volatile修饰,可以禁用指令重排,但要注意jdk5以上的版本volatile才真正有效
五、happens-before
happens-before规定了哪些写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结
线程对volatile变量的写,对接下来其它线程对该变量的读可见
public class ThreadTest {
volatile static int x;
public static void main(String[] args) {
new Thread(() -> {
x = 10;
}, "t1").start();
new Thread(() -> {
System.out.println(x);
}, "t2").start();
}
}
线程解锁m之前对变量的写,对于接下来对m加锁的其它线程对该变量的读可见
public class ThreadTest {
static int x;
static Object m=new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (m){
x = 10;
}
}, "t1").start();
new Thread(() -> {
synchronized (m){
System.out.println(x);
}
}, "t2").start();
}
}
线程start前对变量的写,对该线程开始后对该变量的读可见
public class ThreadTest {
static int x;
public static void main(String[] args) {
x = 10;
new Thread(() -> System.out.println(x), "t2").start();
}
}
线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用t1.isAlive()或t1.join()等待它结束)
public class ThreadTest {
static int x;
public static void main(String[] args) throws InterruptedException {
x = 10;
Thread t1=new Thread(()->{
x=10;
},"t1");
t1.start();
t1.join();
System.out.println(x);
}
}