• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

JVMJVM08(java内存模型[JMM])

武飞扬头像
温文艾尔
帮助1

⭐️写在前面


  • 这里是温文艾尔の学习之路
  • 👍如果对你有帮助,给博主一个免费的点赞以示鼓励把QAQ
  • 👋博客主页🎉 温文艾尔の学习小屋
  • ⭐️更多文章👨‍🎓请关注温文艾尔主页📝
  • 🍅文章发布日期:2022.02.15
  • 👋java学习之路!
  • 欢迎各位🔎点赞👍评论收藏⭐️
  • 🎄新年快乐朋友们🎄
  • 👋jvm学习之路!
  • ⭐️上一篇内容:【JVM】JVM07(类加载阶段详细解析)

1.java内存模型

java内存模型-Java Memory Model的意思,这个要和java内存结构进行区分

简单的说,JMM定义了一套才多线程读写共享数据时(成员便来那个、数组)时,对数据的可见性、有序性和原子性的规则和保障

1.1原子性

java中对静态变量的自增,自减并不是原子操作
例如对i 而言(i为静态变量),实际会产生如下的JVM字节码指令:

getstatic i //获取静态变量i的值
iconst_1 //准备常量1
iadd //自增
putstatic i //将修改后的值存入静态变量    

而对应i–也是类似:

getstatic i //获取静态变量i的值
iconst_1 //准备常量1
isub //自减
putstatic i //将修改后的值存入静态变量   

而java的内存模型如下,完成静态变量的自增,自减需要在主存和内存之间进行数据交换
学新通
如果是单线程以上代码是顺序执行(不会交错),没有问题:

//i的初始值为0
getstatic i //获取静态变量i的值
iconst_1 //准备常量1
iadd //自增 线程内i=1
putstatic i //将修改后的值存入静态变量,静态变量i=1    
getstatic i //获取静态变量i的值,线程内i=1
iconst_1 //准备常量1
isub //自减 线程内i=0
putstatic i //将修改后的值存入静态变量 静态变量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=1  
iconst_1 //线程2-准备常量1
iadd //线程2-自减 线程内i=-1
putstatic i //线程2-将修改后的值存入静态变量,静态变量i=-1   

出现正数的情况:

getstatic i //线程1-获取静态变量i的值,线程内i=0
getstatic i //线程2-获取静态变量i的值,线程内i=0
iconst_1 //线程1-准备常量1
iadd //线程1-自增 线程内i=1
iconst_1 //线程2-准备常量1
iadd //线程2-自增 线程内i=-1
putstatic i //线程2-将修改后的值存入静态变量,静态变量i=-1 
putstatic i //线程1-将修改后的值存入静态变量,静态变量i=1  

java内存模型中保证原子性的方法

synchronized (同步关键字)

语法

synchronized (对象){
    要作为原子操作代码
}

用synchronized解决并发问题:

public class Demo01 {
    static int i = 0;
    static Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int j = 0; j < 5000; j  ) {
                synchronized (obj){
                    i  ;
                }
            }
        });

        Thread t2 = new Thread(()->{
            for (int j = 0; j < 5000; j  ) {
                synchronized (obj){
                    i--;
                }
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(i);
    }
}
学新通
i=0

可以想象Thread1和Thread2两个人,要执行的是一个房间,而object是一把锁,Thread1进入房间(执行)后房间上锁,Thread2要想进入房间除非Thread1出房间

注意上例中Thread1和Thread2必须用synchronized锁住同一个obj对象,如果ti锁住的是m1对象,t2锁住的是m2对象,就好比两个人分别进入了两个不同的房间,没法起到同步的效果

1.2可见性

先来看一个现象,main线程对run变量的修改对于t线程不可见,导致了t线程无法停止:

public class Demo02 {
    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;
    }
}

分析一下:
1.初始状态,t线程刚开始从主内存读取了run的值到工作内存。
学新通
2.因为t线程要频繁从主内存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存中,减少主存中run的访问,提高效率
学新通
3.1秒之后,main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
学新通
解决办法
volatile(易变关键字)

他可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取值。线程操作volatile变量都是直接操作主存

public class Demo02 {
    static volatile boolean run = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while (run){
                //...
            }
        });
        t.start();
        Thread.sleep(1000);
        run = false;
    }
}

学新通
可以看到1s之后程序结束了

1.2.1可见性

上面例子体现的实际就是可见性,他保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况:

上例从字节码理解是这样的

getstatic run //线程t获取 run true
getstatic run //线程t获取 run true
getstatic run //线程t获取 run true
getstatic run //线程t获取 run true
putstatic run //线程main修改run为false,仅此一次
getstatic run //线程t获取 run false

比较一下之前我们将线程安全时举的例子,两个线程一个i ,一个i–

getstatic i //线程1-获取静态变量i的值,线程内i=0
getstatic i //线程2-获取静态变量i的值,线程内i=0
iconst_1 //线程1-准备常量1
iadd //线程1-自增 线程内i=1
putstatic i //线程1-将修改后的值存入静态变量,静态变量i=1  
iconst_1 //线程2-准备常量1
iadd //线程2-自减 线程内i=-1
putstatic i //线程2-将修改后的值存入静态变量,静态变量i=-1   

注意
synchronized语句块即可以保证代码块的原子性,也同时保证代码块内变量的可见性,但缺点是synchronized是属于重量级操作,性能相对更低

如果在前面示例的死循环中加入System.out.println()会发现即使不加volatile修饰符,线程也能正确看到对run变量的修改了,想想为什么?

我们查看源码

    public void println() {
        newLine();
    }


    private void newLine() {
        try {
            synchronized (this) {
                ensureOpen();
                textOut.newLine();
                textOut.flushBuffer();
                charOut.flushBuffer();
                if (autoFlush)
                    out.flush();
            }
        }
        catch (InterruptedIOException x) {
            Thread.currentThread().interrupt();
        }
        catch (IOException x) {
            trouble = true;
        }
    }
学新通

发现输出语句是加了synchronized锁

1.3有序性

执行下列代码

int num = 0;
boolean ready = false;

//线程1执行此方法
public void actor1(I_Result r){
    if(ready){
        r.r1 = num   num;
    }else{
        r.r1 = 1;
    }
}

//线程2执行此方法
public void actor2(I_Result r){
    num = 2;
    ready = true;
}
学新通

I_Result是一个对象,有属性r1用来保存结果,那么可能的结果有几种呢?
除了结果为1,4的情况外,结果还有可能为0
这种情况为:
线程2执行ready=true,切换到线程1,进入if分支,相加为0,再切回线程2执行num=2
这种现象叫做指令重排,是JIT编译器在运行时做的一些优化

1.3.1解决方法

volatile修饰的变量可以禁用指令重排

1.3.2有序性的理解

同一个线程内。JVM会在不影响正确性的前提下,可以调整语句的执行顺序

static int i;
static int j;
//在某个线程内执行如下赋值操作
i = ...;//较为耗时的操作
j = ...;

可以看到至于是先执行i还是先执行j,对最终结果不会产生影响。所以上面代码真正执行时,即可以是

i = ...;//较为耗时的操作
j = ...;

也可以是

j = ...;
i = ...;//较为耗时的操作

这种特性称之为【指令重排】,多线程下【指令重排】会影响正确性,例如注定的double-checked locking模式实现单例

public class Singleton {
    public Singleton() {}
    private static Singleton INSTANCE = null;
    public static Singleton getInstance(){
        //实例没创建,才会进入内部的synchronized代码块
        if (INSTANCE == null){
            synchronized (Singleton.class){
                //也许有其他线程已经创建实例,所以再判断一次
                if (INSTANCE == null){
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}
学新通

以上的实现特点是:

  • 懒惰实例化
  • 首次使用getInstance()才使用synchronized加锁,后续使用时无需加锁

但在多线程环境下,上面的代码还是有问题的,INSTANCE = new Singleton();对应的字节码为:

new   #2
dup
invokespecial #3
putstatic #4

其中4 7两步的顺序是不固定的,也许jvm会优化为:现将引用地址赋值给INSTANCE变量后,再执行构造方法,如果两个线程t1,t2按如下时间序列执行:

时间1 t1 线程执行到INSTANCE = new Singleton();
时间2 t1 线程分配空间,为Singleton对象生成了引用地址(0处)
时间3 t1线程将引用地址赋值给INSTANCE, 这时INSTANCE != null (7处)
时间4 t2 线程进入getInstance()方法,发现INSTANCE != null (synchronized块外) ,直接返回
INSTANCE 时间5 t1 线程执行Singleton的构造方法(4处)

这是t1还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么t2拿到将是一个未初始化完毕的单例

对INSTANCE使用volatile修饰即可,可以使用指令重排,但要注意在JDK5以上的版本的volatile才会真正有效

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfiijgf
系列文章
更多 icon
同类精品
更多 icon
继续加载