JMM java内存模型,它是在多线程访问共享数据时,提供原子性、可见性有序性的规则和保障。
原子性
原子性:一个原子性操作可以包含一条或多条指令,一个原子性操作中的所有指令要么都不执行,要么全部执行且不会收到其他线程操作的影响。
package com.tech.jmm;
/**
* @author lw
* @since 2021/12/6
*/
public class A {
static int i=0;
static Object o=new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 50000; j++) {
++i;
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 50000; j++) {
--i;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
执行后,输出结果会出现多种情况,这是因为 ++i --i并不是原子性操作导致。
++i对应的jvm字节码指令
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 加法
putstatic i // 将修改后的值存入静态变量i
--i对应的jvm字节码指令
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 减法
putstatic i // 将修改后的值存入静态变量i
++i 和 --i都是对应多条JVM指令,在多线程执行下可能会出现如下情况
出现负数:
// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
出现正数:
// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
由于以上情况的存在,会出现多种运行结果。
通过加锁,来实现原子性保障
package com.tech.jmm;
/**
* @author lw
* @since 2021/12/6
*/
public class A {
static int i=0;
static Object o=new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 50000; j++) {
synchronized (o){
++i;
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 50000; j++) {
synchronized (o){
--i;
}
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
多次运行后能得到正确执行结果
可见性
可见性:写线程对于共享变量的修改对于共享变量的修改,对其他线程是可见的,也就是读线程可以读取到共享变量的最新值。
package com.tech.jmm;
/**
* @author lw
* @since 2021/12/6
*/
public class B {
static boolean run=true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
long start=System.currentTimeMillis();
while (run){
}
System.out.println(System.currentTimeMillis()-start);
}).start();
Thread.sleep(1000);
run=false;
}
}
新建的线程需要频繁的从主内存读取共享变量run的值,JIT即时编译器对热点代码进行优化直接将主内存run的值保存到工作内存的高速缓存中,减少对主内存的访问提升性能。当主线程修改run值后,由于新建的线程使用的是工作内存中读取run的值,读到的run的值是true,所以程序一直循环执行。
对变量run加volatile修饰,确保可见性
package com.tech.jmm;
/**
* @author lw
* @since 2021/12/6
*/
public class B {
volatile static boolean run = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
long start = System.currentTimeMillis();
while (run) {
}
System.out.println(System.currentTimeMillis() - start);
}).start();
Thread.sleep(1000);
run = false;
}
}
程序正常执行结束。
volatile 可以确保修改的可见性,当写线程修改共享变量值后,会立刻刷新到主内存,同时让其他读线程工作内存中变量的值失效,去主内存获取变量最新的值。
有序性
为了提升程序的执行性能,在不影响单线程执行结果的前提下,执行的指令会发生重排序。
package com.tech.jmm;
/**
* DCL双重检测锁 单例模式
* @author lw
* @since 2021/12/7
*/
public class Singleton {
private static Singleton INSTANCE=null;
private Singleton(){}
public static Singleton getInstance(){
if(INSTANCE==null){
synchronized (Singleton.class){
if(INSTANCE==null){
INSTANCE=new Singleton();
}
}
}
return INSTANCE;
}
}
基于双层检测锁机制实现的单例模式
package com.tech.jmm;
/**
* DCL双重检测锁 单例模式
* @author lw
* @since 2021/12/7
*/
public class Singleton {
private static Singleton INSTANCE=null;
private Singleton(){}
public static Singleton getInstance(){
if(INSTANCE==null){
synchronized (Singleton.class){
if(INSTANCE==null){
INSTANCE=new Singleton();
}
}
}
return INSTANCE;
}
}
这个单例模式,看着没问题,但是由于在执行 INSTANCE=new Singleton(); 时内部存在重排序,会产生一定的问题,INSTANCE=new Singleton();对应的字节码指令如下:
0: new #2 // class cn/itcast/jvm/t4/Singleton
3: dup
4: invokespecial #3 // Method "<init>":()V
7: putstatic #4 // Field
INSTANCE:Lcn/itcast/jvm/t4/Singleton
0:为对象分配内存空间,并生成引用地址
3:将引用地址复制一份
4:执行对象的初始化方法,进行初始化
7:将对象的引用赋值给变量
总体流程是这样三个步骤:先为对象分配内存空间并生成引用,对象初始化,将对象的引用赋值给变量。
如果第二步和第三步发生重排序,当执行了将对象的引用赋值给变量,但是还没有执行对象初始化,如果此时另外一个线程调用获取对象实例的方法,检测到对象不为空,直接返回单例对象,但是此时对象还没有初始化完成,在使用时会出现多种问题。
解决方案:
在变量前添加volatile修饰,禁止创建对象时的指令重排
package com.tech.jmm;
/**
* DCL双重检测锁 单例模式
* @author lw
* @since 2021/12/7
*/
public class Singleton {
private volatile static Singleton INSTANCE=null;
private Singleton(){}
public static Singleton getInstance(){
if(INSTANCE==null){
synchronized (Singleton.class){
if(INSTANCE==null){
INSTANCE=new Singleton();
}
}
}
return INSTANCE;
}
}
Happens-Before规则
happens-before规则定义了哪些操作的写对于其他线程的读是可见的,它是保障可见性和有序性的一套规则总结。
解锁前的写操作happens-before加锁后的读操作
package com.tech.jmm.before;
/**
* 解锁前的写 happens-before 加锁后的读
* @author lw
* @since 2021/12/7
*/
public class LockBefore {
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();
}
}
线程t2读取的值为10
volatile变量的写操作 happens-before 其他线程对于该变量的读操作
package com.tech.jmm.before;
/**
* volatile变量的写操作 happens-before 其他线程对于该变量的读操作
* @author lw
* @since 2021/12/7
*/
public class VolatileBefore {
volatile static int x;
public static void main(String[] args) {
new Thread(()->{
x=10;
},"t1").start();
new Thread(()->{
System.out.println(x);
},"t2").start();
}
}
线程t2读取的值为10
线程start前变量的写,happens-before,该线程中的读
package com.tech.jmm.before;
/**
* 线程start前变量的写 happens-before变量的读
* @author lw
* @since 2021/12/7
*/
public class StartBefore {
static int x=0;
public static void main(String[] args) {
x=10;
new Thread(()->{
System.out.println(x);
},"t2").start();
}
}
线程t2读取的值为10
对一个线程中断前进行共享变量的写,happens-before,所有知道线程被中断的线程中的读取
package com.tech.jmm.before;
/**
* 对一个线程中断前进行共享变量的写,happens-before,所有知道线程被中断的线程中的读取
* @author lw
* @since 2021/12/7
*/
public class InterruptBefore {
static int x=0;
public static void main(String[] args) {
Thread t2 = new Thread(() -> {
while (true){
if(Thread.currentThread().isInterrupted()){
System.out.println(x);
break;
}
}
}, "t2");
t2.start();
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
x=10;
t2.interrupt();
}, "t1");
t1.start();
while (!t2.isInterrupted()){
Thread.yield();
}
System.out.println(x);
}
}
线程t2 main读取的值都为10