ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

Java对象模型

2020-02-06 15:05:20  阅读:221  来源: 互联网

标签:Java Thread 对象 模型 int volatile 内存 new 线程


 


1 Java内存模型

JMM (Java Memory Model)

  • 是一组规范,需要各个JVM的实现来遵守JMM规范,以便于开发者可以利用这些规范,更方便地开发多线程程序。
  • 如果没有这样的一个JMM内存模型来规范,那么很可能经过了不同JVM的不同规则的重排序之后,导致不同的虚拟机上运行的结果不一样,那是很大的问题。
  • volatile、synchronized、Lock等的原理都是JMM
  • 如果没有JMM,那就需要我们自己指定什么时候用内存栅栏等,那是相当麻烦的,幸好有了JMM,让我们只需要用同步工具和关键字就可以开发并发程序。
  • 最重要的3点内容:重排序、可见性、原子性。

为什么需要JMM

  • C语言不存在内存模型的概念
  • 依赖处理器,不同处理器结果不一样
  • 无法保证并发安全
  • 需要一个标准,让多线程运行的结果可预期

 


2 重排序

有序性

  • Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性
  • volatile、 synchronized、Lock

有序性 happens-before原则(如果两个操作的执行次序无法从happens-before原则推导出来,那么就不能保证他们的有序性,虚拟机就可以随意的对他们进行重排序)

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  2. 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
/**
 * 演示重排序的现象 “直到达到某个条件才停止”,测试小概率事件
 */
public class OutOfOrderExecution {
    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;
        while (true) {
            i++;
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            CountDownLatch latch = new CountDownLatch(3);
            Thread one = new Thread(() -> {
                try {
                    latch.countDown();
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                a = 1;
                x = b;
            });
            Thread two = new Thread(() -> {
                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 == 0 && y == 0) {
                System.out.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }
    }
}

 

什么是重排序:在线程1内部的两行代码的实际执行顺序和代码在Java文件中的顺序不一致,代码指令并不是严格按照代码语句顺序执行的,它们的顺序被改变了,这就是重排序,这里被颠倒的是y=a和b=1这两行语句。

重排序的3种情况

  • 编译器优化:包括JVM,JIT编译器等
  • CPU指令重排:就算编译器不发生重排,CPU也可能对指令进行重排
  • 内存的 “重排序” :线程A的修改线程B却看不到,引出可见性问题

3 可见性

导致共享变量在线程间不可见的原因

  • 线程交叉执行
  • 重排序结合线程交叉执行
  • 共享变量更新后的值没有在工作内存与主存间及时更新

3.1 案例:演示什么是可见性问题

/**
 * 演示可见性带来的问题
 */
public class FieldVisibility {
    //volatile:每次读取都强制要求读取的是线程最新修改后的值
    volatile int a = 1;
    volatile 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) {
        //b=3;a=1  //可见性
        //b=2;a=3
        //b=2;a=1
        //b=3;a=3
        while (true) {
            FieldVisibility test = new FieldVisibility();
            new Thread(() -> {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                test.change();
            }).start();

            new Thread(() -> {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                test.print();
            }).start();
        }
    }
}

3.2 为什么会有可见性问题

CPU有多级缓存,导致读的数据过期

  • 高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在CPU和主内存之间就多了Cache层。
  • 线程间的对于共享变量的可见性问题不是直接由多核引起的,而是由多缓存引起的。
  • 如果所有的核心都只用一个缓存,那么也就不存在内存可见性问题了。
  • 每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存中。所以会导致有些核心读取的值是一个过期的值。

3.3 JMM的抽象:主内存和本地内存

什么是主内存和本地内存

  • Java作为高级语言,屏蔽了这些底层细节,用JMM定义了一套读写内存数据的规范,虽然我们不再需要关心一级缓存和二级缓存的问题,但是,JMM抽象了主内存和本地内存的概念。
  • 这里说的本地内存并不是真的是一块给每个线程分配的内存,而是JMM的—个抽象,是对于寄存器、一级缓存、二级缓存等的抽象。

主内存和本地内存的关系,JMM有以下规定

  1. 所有的变量都存储在主内存中,同时每个线程也有自己独立的工作内存,工作内存中的变量内容是主内存中的拷贝
  2. 线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后再同步到主内存中
  3. 主内存是多个线程共享的,但线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转来完成
  4. 所有的共享变量存在于主内存中,每个线程有自己的本地内存,而目线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题

3.4 Happens-Before原则

Happens-Before规则有哪些

  1. 单线程规则
  2. 锁操作(synchronized和Lock)
  3. volatile变量
  4. 线程启动
  5. 线程join
  6. 传递性
  7. 中断
  8. 构造方法

3.4.1 单线程规则

3.4.2 锁操作(synchronized和Lock)

JMM关于synchronized的两条规定

  • 线程解锁前,必须把共享变量的最新值刷新到主内存
  • 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意,加锁与解锁是同一把锁)

3.4.3 volatile的可见性

通过加入内存屏障和禁止重排序优化来实现

  • 对volatile变量写操作时,会在写操作后加入一条 store屏障指令,将本地内存中的共享变量值刷新到主内存
  • 对 volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量

3.4.4 线程启动

3.4.5 线程join

3.4.6 传递性

3.4.7 中断

3.4.8 构造方法

3.4.9 工具类的 Happens-Before原则

3.5 volatile关键字

3.5.1 volatile是什么

  • volatile是一种同步机制,比synchronized或者Lock相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为
  • 如果一个变量被修饰成volatile,那么JVM就知道了这个变量可能会被并发修改
  • volatile开销小,相应的能力也小,虽然说volatile是用来同步的保证线程安全的,但是volatile做不到synchronized那样的原子保护,volatile仅在很有限的场景下才能发挥作用

3.5.2 volatile的适用场合 

  • 不适用:a++
  • 适用场合1:boolean flag,如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全
  • 适用场合2:作为刷新之前变量的触发器

 

 

/**
 * volatile适用的情况1
 */
public class UseVolatile implements Runnable {
    volatile boolean done = false;
    AtomicInteger realA = new AtomicInteger();
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            setDone();
            realA.incrementAndGet();
        }
    }
    private void setDone() {
        done = true;
    }
    public static void main(String[] args) throws InterruptedException {
        Runnable r =  new UseVolatile();
        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(((UseVolatile) r).done);
        System.out.println(((UseVolatile) r).realA.get());
    }
}
/**
 * 适用场合2:作为刷新之前变量的触发器
 */
public class FieldVisibility {
    int a = 1;
    int ab = 1;
    int abc = 1;
    int abcd = 1;
    volatile int b = 2;
    private void change() {
        a = 3;
        ab = 4;
        abc = 5;
        abcd = 6;
        //b充当触发器的作用 写
        b=0;
    }
    private void print() {
        //这个时候打印出来的值一定是最新的值 读
        //happens-before:读取的时候一定能够看到写入之前的所有操作
        if (b==0) {
            System.out.println("a=" + a + ";ab=" + ab+ ";abc=" + abc+ ";abcd=" + abcd);
        }
    }
    public static void main(String[] args) {
        while (true) {
            FieldVisibility test = new FieldVisibility();
            new Thread(() -> {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                test.change();
            }).start();
            new Thread(() -> {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                test.print();
            }).start();
        }
    }
}

3.5.3 不适用于volatile的场景

/**
 * 不适用于volatile的场景 a++
 */
public class NoVolatile implements Runnable {
    volatile int a;
    AtomicInteger realA = new AtomicInteger();
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            a++;
            realA.incrementAndGet();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Runnable r =  new NoVolatile();
        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(((NoVolatile) r).a);
        System.out.println(((NoVolatile) r).realA.get());
    }
}
/**
 * volatile不适用的情况2
 */
public class NoVolatile implements Runnable {
    volatile boolean done = false;
    AtomicInteger realA = new AtomicInteger();
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            flipDone();
            realA.incrementAndGet();
        }
    }
    private void flipDone() {
        //这里的done取决于之前的状态
        done = !done;
    }
    public static void main(String[] args) throws InterruptedException {
        Runnable r =  new NoVolatile();
        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        //正常:偶数次false 基数次true
        //
        System.out.println(((NoVolatile) r).done);
        System.out.println(((NoVolatile) r).realA.get());
    }
}

3.5.4 volatile的两点作用

  1. 可见性:读一个 volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个volatile属性会立即刷入到主内存
  2. 禁止指令重排序优化:解决单例双重锁乱序问题(https://blog.csdn.net/qq_40794973/article/details/104165984#t6

3.5.5 volatile和synchronized的关系

volatile在这方面可以看做是轻量版的synchronized:如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。

3.5.6 用volatile修正重排序问题

OutOfOrderExecution类加了volatile后,就不会出现 (0,0) 的情况了

public class OutOfOrderExecution {
    private volatile static int x = 0, y = 0;
    private volatile 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);
            Thread one = new Thread(() -> {
                try {
                    latch.countDown();
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                a = 1;
                x = b;
            });
            Thread two = new Thread(() -> {
                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 == 0 && y == 0) {
                System.out.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }
    }
}

3.5.7 volatile小结 

  • volatile 修饰符适用于以下场景:某个属性被多个线程共享,其中有—个线程修改了此属性,其他线程可以立即得到修改后的值,比如boolean flag;或者作为触发器,实现轻量级同步
  • volatile 属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的
  • volatile 只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序
  • volatile 提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile属性不会被线程缓存,始终从主存中读取
  • volatile 提供了 happens-before 保证,对volatile变量ν的写入 happens-before 所有其他线程后续对v的读操作
  • volatile 可以使得 long 和 double 的赋值是原子的

 

4.2.6 能保证可见性的措施

  • 除了volatile可以让变量保证可见性外,synchronized、Lock、并发集合、Thread.join()和 Thread.start()等都可以保证可见性
  • 具体看 happens-before 原则的规定

4.2.7 升华:对synchronized可见性的正确理解

public class FieldVisibilityABCD {
    int a = 1;
    int b = 2;
    int c = 2;
    int d = 2;
    private void change() {
        a = 3;
        b = 4;
        c = 5;
        synchronized (this) {//近朱者赤
            d = 6;
        }
    }
    private void print() {
        synchronized (this) {
            int aa = a;
        }
        int bb = b;
        int cc = c;
        int dd = d;
        System.out.println("b=" + b + ";a=" + a);
    }
    public static void main(String[] args) {
        while (true) {
            FieldVisibilityABCD test = new FieldVisibilityABCD();
            new Thread(() -> {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                test.change();
            }).start();
            new Thread(() -> {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                test.print();
            }).start();
        }
    }
}

4 原子性

4.1 什么是原子性

一系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分割的。

原子性:Atomic包、CAS算法、synchronized、Lock 

4.2 Java中的原子操作有哪些?

  • 除long和double之外的基本类型(int, byte, boolean, short, char, float)的赋值操作
  • 所有引用reference的赋值操作,不管是32位的机器还是64位的机器
  • java.util.concurrent.atomic.*包中所有类的原子操作

4.3 long和doublel的原子性

  • 问题描述:官方文档、对于64位的值的写入,可以分为两个32位的操作进行写入、读取错误、使用volatile解决
  • 结论:在32位上的JVM上,long和double的操作不是原子的,但是在64位的JVM上是原子的
  • 实际开发中:商用Java虚拟机中不会出现

官方文档:https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html


17.7. Non-atomic Treatment of double and long

For the purposes of the Java programming language memory model, a single write to a non-volatile long or double value is treated as two separate writes: one to each 32-bit half. This can result in a situation where a thread sees the first 32 bits of a 64-bit value from one write, and the second 32 bits from another write.

Writes and reads of volatile long and double values are always atomic.

Writes to and reads of references are always atomic, regardless of whether they are implemented as 32-bit or 64-bit values.

Some implementations may find it convenient to divide a single write action on a 64-bit long or double value into two write actions on adjacent 32-bit values. For efficiency's sake, this behavior is implementation-specific; an implementation of the Java Virtual Machine is free to perform writes to long and double values atomically or in two parts.

Implementations of the Java Virtual Machine are encouraged to avoid splitting 64-bit values where possible. Programmers are encouraged to declare shared 64-bit values as volatile or synchronize their programs correctly to avoid possible complications.


出于Java编程语言内存模型的目的,一次对非易失性long或 double值的写入被视为两次单独的写入:一次写入每个32位的一半。这可能导致线程从一次写入中看到64位值的前32位,而从另一次写入中看到后32位的情况。

volatile longdoublevalue的写入和读取 始终是原子的。

引用的写入和读取始终是原子的,无论它们是实现为32位还是64位值。

一些实现可能会发现将对64位longdouble值的单个写操作划分为对相邻32位值的两个写操作很方便。为了提高效率,此行为是特定于实现的;Java虚拟机的实现可以自由执行原子和两部分的写入longdouble值。

鼓励Java虚拟机的实现避免在可能的情况下拆分64位值。鼓励程序员将共享的64位值声明为volatile或正确同步其程序,以避免可能的复杂性。

4.4 原子操作 + 原子操作 != 原子操作

  • 简单地把原子操作组合在一起,并不能保证整体依然具有原子性
  • 比如我去ATM机两次取钱是两次独立的原子操作,但是期间有可能银行卡被借给女朋友,也就是被其他线程打断并被修改。
  • 全同步的HashMap也不完全安全

5 面试常见问题

5.1 单例模式的8种写法

https://yuanyu.blog.csdn.net/article/details/104165984

5.2 讲一讲什么是Java内存模型

 

5.3 volatile和 synchronized的异同?

 

5.4 什么是原子操作?Java中有哪些原子操作?生成对象的过程是不是原子操作?

 生成对象的过程

  1. 新建一个空的Person对象
  2. 把这个对象的地址指向p
  3. 执行Person的构造函数

5.5 什么是内存可见性?

 

5.6 64位的double和long写入的时候是原子的吗

java并没有规定它们一定是原子的

32为机器上不是原子的,64为机器上是原子的

但是在商用计算机通常不需要考虑(都是64的)


https://www.cnblogs.com/null-qige/p/9481900.html

底层_码农 发布了530 篇原创文章 · 获赞 698 · 访问量 110万+ 他的留言板 关注

标签:Java,Thread,对象,模型,int,volatile,内存,new,线程
来源: https://blog.csdn.net/qq_40794973/article/details/104193816

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有