第十二章、 Java内存模型JMM——底层原理

1、到底什么叫“底层原理”?本章研究的内容是什么?

1.1 从Java代码到CPU指令

①最开始,我们编写的Java代码,是*.java文件
②在编译(javac命令)后,从刚才的*.java文件会变出一个新的Java字节码文件(*.class)
③JVM会执行刚才生成的字节码文件(*.class),并把字节码文件转化为机器指令
④机器指令可以直接在CPU上运行,也就是最终的程序执行

1.2 JVM实现会带来不同的“翻译”,不同的CPU平台的机器指令又千差万别,无法保证并发安全的效果一致

1.3 因此引入内存模型:转换过程的规范、原则

2、三兄弟:JVM内存结构 VS Java内存模型 VS Java对象模型

整体方向:

  • JVM内存结构,和Java虚拟机的运行时区域有关。如堆和栈
  • Java内存模型,和Java的并发编程有关
  • Java对象模型,和Java对象在虚拟机中的表现形式有关

2.1 JVM内存结构

  • 堆(heap):是运行时数据区中占用最大的。存储对象的实例
  • 虚拟机栈/Java栈(VM stack):保存各个基本类型、对象引用
  • 方法区(method):存放static的静态变量/类/常量,以及永久引用
  • 本地方法栈:存放与本地方法(native)相关的
  • 程序计数器

2.2 Java对象模型

  • Java对象自身的存储模型
  • JVM会给这个类创建一个instanceKlass保存在方法区,用来在JVM层表示该Java类。
  • 当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据。

3、JMM是什么

3.1 为什么需要JMM

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

3.2 JMM是规范

  • Java Memory Model
  • JMM是一组规范,需要各个JVM的实现来遵守JMM规范,以便于开发者可以利用这些规范,更方便地开发多线程程序
  • 如果没有这样的一个JMM内存模型来规范,那么很可能经过了不同JVM的不同规则的重排序之后,导致不同的虚拟机上运行的结果不一样,那是很大的问题

3.3 JMM是工具类和关键字的原理

  • volatile、synchronized、Lock等的原理都是JMM
  • 如果没有JMM,那就需要我们自己指定什么时候用内存栅栏等,那是相当麻烦的,幸好有了JMM,让我们只需要用同步工具和关键字就可以开发并发程序

3.4 最重要的3点内容:重排序、可见性、原子性为什么需要JMM

4、重排序

4.1 重排序的代码案例

/**
 * OutOfOrderExecution
 *
 * @author venlenter
 * @Description: 演示重排序的现象
 * “直到达到某个条件才停止”,测试小概率事件
 * @since unknown, 2020-05-20
 */
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 {
        Thread one = new Thread(new Runnable() {
            @Override
            public void run() {
                a = 1;
                x = b;
            }
        });
        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);
    }
}

4.2 重排序分析

这4行代码的执行顺序决定了最终x和y的结果,一共有3种情况:

  • 1、 a=1;x=b(0);b=1;y=a(1),最终结果是x=0,y=1
Thread-one执行完后,Thread-two再执行
  • 2、 b=1;y=a(0);a=1;x=b(1),最终结果是x=1,y=0
Thread-two执行完后,Thread-one再执行
  • 3、 b=1;a=1;x=b(1);y=a(1),最终结果是x=1,y=1
/**
 * OutOfOrderExecution
 *
 * @author venlenter
 * @Description: 演示重排序的现象
 * “直到达到某个条件才停止”,测试小概率事件
 * @since unknown, 2020-05-20
 */
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;
        for (; ; ) {
            i++;
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            CountDownLatch latch = new CountDownLatch(1);

            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;
                }
            });
            one.start();
            two.start();
            latch.countDown();
            one.join();
            two.join();

            String result = "第" + i + "次(" + x + "," + y + ")";
            System.out.println(result);
            if (x == 1 && y == 1) {
                break;
            }
        }
    }
}
//输出结果
...
第230次(0,1)
第231次(1,1)
  • 重排序出现的情况(x=0,y=0)
Thread-1
a = 1;
x = b;
——————————————————————————
Thread-2
b = 1;
y = a;


y=a;  #Thread2的2行代码被重排序了,同时中间插入了Threa1
a=1;
x=b;
b=1

4.3 什么是重排序:

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

4.4 重排序的好处:提高处理速度

  • 对比重排序前后的指令优化 

4.5 重排序的3种情况:编译器优化、CPU指令重排、内存的“重排序”

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

5、可见性

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

(1)案例一

  • 代码演示
/**
 * FieldVisibility
 *
 * @author venlenter
 * @Description: 演示可见性带来的问题
 * @since unknown, 2020-05-23
 */
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();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.change();
                }
            }).start();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.print();
                }
            }).start();
        }
    }
}
//输出可能结果
b=2;a=1(println线程先执行)
b=3;a=3(先执行完全部change,再执行println)
b=2;a=3(change执行a=3后,切换到println线程)


第四种情况:
b=3;a=1(出现了可见性问题--概率比较小,但确实发生了):
没给b加volatile,那么有可能出现a=1,b=3.因为a虽然被修改了,但是其他线程不可见,而b恰好其他线程可见,这就造成了b=3,a=1

(2)案例二

  • 代码
public class FieldVisibility {
    int x = 0;
    public void writeThread() {
        x = 1;
    }
    public void readerThread() {
        int r2 = x;
    }
}
  • 案例二分析:

  • 可见性问题出现问题原因:
①主内存中原x=0,线程1和线程2分别读取了x=0
②线程1在工作内存中赋值x=1,但还没有写入到主内存
③此时线程2的本地内存中x还是0,所以导致了可见性问题

(3)用volatile解决问题

  • 代码
/**
 * FieldVisibility
 *
 * @author venlenter
 * @Description: 演示可见性带来的问题
 * @since unknown, 2020-05-23
 */
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) {
        while (true) {
            FieldVisibility test = new FieldVisibility();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.change();
                }
            }).start();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.print();
                }
            }).start();
        }
    }
}
  • 分析

①使用了volatile,线程1在工作内存中修改了x=1后,会强制flush到主内存
②当线程2要读取x/使用旧的x的时候,会判断x为失效,同时重新从主内存中读取进来,则x为新的1

5.2、为什么会有可见性问题

RAM是主内存
registers是寄存器
core假设是多核CPU
  • CPU读取寄存器registers中缓存——>registers读取L1 cache级缓存——>L2——>L3——>主内存RAM

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

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

5.3、JMM的抽象:主内存和本地内存

5.3.1 什么是主内存和本地内存

  • Java作为高级语言,屏蔽了这些底层细节,用JMM定义了一套读写内存数据的【规范】,虽然我们【不再需要关心一级缓存和二级缓存】的问题,但是,JMM抽象了主内存和本地内存的概念
  • 这里说的本地内存【并不是真的是一块给每个线程分配的内存】,而是JMM的一个抽象,是对于寄存器、一级缓存、二级缓存等的【抽象】
  • 以上面core、regisiters的那张图来说:(registers、L1、L2是线程的本地内存)(L3、RAM是线程共享的)

5.3.2 主内存和本地内存的关系

JMM有以下规定:

  • 【所有的变量】都存储在【主】内存中,同时【每个线程】也有自己【独立】的【工作内存】,工作内存中的变量内容是主内存中的【拷贝】
  • 线程【不能直接读写主内存中】的变量,而是只能【操作自己工作内存】中的变量,然后再【同步】到主内存中
  • 【主内存】是【多个线程共享】的,但【线程间不共享工作内存】,如果线程间需要【通信】,必须借助【主内存中转】来完成

所有的【共享变量存在于主内存】中,每个【线程有自己的本地内存】,而且【线程读写共享数据也是通过本地内存交换】的,所以才导致了【可见性问题】

5.4、Happens-Before原则

  • 什么是happens-before:(解决可见性问题)在时间上,动作A发生在动作B之前,B保证能看见A,这就是happens-before
  • Happens-Before原则有哪里?

1. 单线程规则

  • 同个线程(同个工作内存)内,前面修改的变量对后面的操作是可见的。但不影响重排序
  • 例如
 int a = 1;
    int b = 2;
    private void change() {
        a = 3;
        b = a;
    }
    
在同个线程执行change()的时候,a=3先执行,则后面b=a肯定为3,因为这里使用到的是同个工作内存,a=3和b=3都是在该线程的同个工作内存中执行的,所以可见

但如果发生了重排序,也就是b=a先执行了,这是允许的,这里b=a=1初始值

2. 锁操作(synchronized和Lock)

3. volatile变量

  • 理解:只要TheadA volatile变量是已经写入了,那么ThreadB读取就肯定可以读取到最新的结果
  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);
    }
    
如果change()先执行,由于a、b被volatile,volatile也会防止重排序导致错误结果,所以a=3肯定在b=a之前执行。

所以当change()先执行完后,再执行print(),肯定可以得到正确的结果,不会出现change线程和print线程穿插导致变量值错乱的问题

4. 线程启动

  • ThreadB执行的时候,可以看到ThreadA之前的操作

5. 线程join

  • 在ThreadA(例如主线程为main()方法)中执行了ThreadB.join,则ThreadA会等待ThreadB执行完毕后,才执行下面的statement1的操作逻辑
  • 当ThreadB执行完毕后,下面的statement1也可以看到statement1的变化

6. 传递性:如果hb(A,B)而且hb(B,C),那么可以推出hb(A,C)

  • 假设背景为main主线程中有ThreadA、ThreadB、ThreadC的执行,如果ThreadA和ThreadB遵循happen-before原则,ThreadB和ThreadC也遵循happens-before,则可以推出hb(A,C)

7. 中断:一个线程被其他线程interrupt时,那么检测中断(isInterrupted)或者抛出InterruptedException一定能看到

8. 构造方法:对象构造方法的最后一行指令happens-before于finalize()方法的第一行指令

  • finalize()已不推荐使用

9. 工具类的Happens-Before原则

  • (1)线程安全的容器get一定能看到在此之前的put等存入动作
如线程安全的ConcurrentHashMap的get和put
  • (2)CountDownLatch
CountDownLatch latch = new CountDownLatch(1);
Thread one = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            latch.countDown();
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        a = 1;
        x = b;
    }
});
one.start();
latch.countDown();

当执行CountDownLatch的countDown(),Thread one才能从await中唤醒,继续执行下面的a=1;x=b
  • (3)Semaphore:类似CountDownLatch
  • (4)Future:可以去后台执行,并拿到一个线程执行结果的类。Future的get是拿到Future的执行结果,get对于之前的执行结果是可见的(不用过多关注,默认保证的)
  • (5)线程池:我们会向线程池提交许多任务,然后在提交的任务中,每个任务都可以看到在提交之前的所有的执行结果(不用过多关注,默认保证的)
  • (6)CyclicBarrier:CountDownLatch
①CyclicBarrier cyclicBarrier1 = new CyclicBarrier(1);
②cyclicBarrier1.await();
③xxx
④cyclicBarrier1.reset(); //当执行了reset后,才能从②await中唤起,继续执行③的代码

案例:happens-before演示

  • happens-before有一个原则是:如果A是对volatile变量的写操作,B是对同一个变量的读操作,那么hb(A,B)
  • 分析这四种情况:
    int a = 1;
    int b = 2;
    private void change() {
        a = 3;
        b = a;
    }
    private void print() {
        System.out.println("b=" + b + ";a=" + a);
    }
    
//输出可能结果
b=2;a=3(change执行a=3后,切换到println线程)
b=2;a=1(println线程先执行)
b=3;a=3(先执行完全部change,再执行println)

第四种:b=3;a=1(出现了可见性问题--概率比较小,但确实发生了):
没给b加volatile,那么有可能出现a=1,b=3.因为a虽然被修改了,但是其他线程不可见,而b恰好其他线程可见,这就造成了b=3,a=1
  • 改进:之前是对a、b都加了volatile,实际上在该场景,只要对b加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);
    }
  • 近朱者赤:给b加了volatile,不仅b被影响,也可以实现轻量级同步
  • b之前的写入(对应代码b=a)对读取b后的代码(print b)都可见,所以在writerThread里对a的赋值,一定会对readerThread里的读取可见,所以这里的【a即使不加volatile,只要b读到的是3,就可以由happens-before原则保证了print a读到的也都是3而不可能读到1

5.5、volatile关键字

5.5.1 volatile是什么

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

5.5.2 volatile的适用场合

(1)不适用a++

/**
 * NoVolatile
 *
 * @author venlenter
 * @Description: 不适用于volatile的场景
 * @since unknown, 2020-05-26
 */
public class NoVolatile implements Runnable {
    volatile int a;
    AtomicInteger realA = new AtomicInteger();

    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);

    }
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            a++;
            realA.incrementAndGet();
        }
    }
}
//输出结果
19855
20000

(2)适用场景:volatile的变量,不依赖之前的值。如果依赖之前的值如a++(先读a,再+),就会有问题。如果只是对变量进行覆盖赋值(不依赖之前的值),则适用

(3)适用场景1:boolean flag,如果一个共享变量自始自终只【被各个线程赋值】,而没有其他的操作(对比、取值),那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全1

(4)适用场景2:作为刷新之前变量的触发器

Map configOptions;
char[] configText;
volatile boolean initialized = false;

//Thread A
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;

//Thread B
## 当在ThreadA中initialized设置为true,则在ThreadB中就跳过while,同时因为volatile的happens-before,则在ThreadA的initialized赋值操作前的configOptions肯定已经初始化完毕了
while (!initialized) {
    sleep();
}
//use configOptions

5.5.3 volatile的作用:可见性、禁止重排序

(1)可见性:读volatile变量时会去【主内存读取最新值】,写一个volatile属性会【立即刷入到主内存】

(2)禁止指令【重排序】优化:解决单例双重锁乱序问题

5.5.4 volatile和synchronized的关系?

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

5.5.5 学以致用:用volatile修正重排序问题

  • OutOfOrderExecution类加了volatile后,用于不会出现(0,0)的情况了
/**
 * OutOfOrderExecution
 *
 * @author venlenter
 * @Description: 演示重排序的现象
 * “直到达到某个条件才停止”,测试小概率事件
 * @since unknown, 2020-05-20
 */
public class OutOfOrderExecution {
    //加上volatile,则不会出现(0,0)了
    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(1);

            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;
                }
            });
            one.start();
            two.start();
            latch.countDown();
            one.join();
            two.join();

            String result = "第" + i + "次(" + x + "," + y + ")";
            System.out.println(result);
            if (x == 0 && y == 0) {
                break;
            }
        }
    }
}

5.5.6 volatile小结

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

5.6、能保证可见性的措施

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

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

  • synchronized不仅保证了原子性,还保证了【可见性】
  • synchronized不仅让被保护的代码安全,还让其之前的代码执行结果可见

6、原子性

6.1、什么是原子性

  • 一系列操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分割的
  • 银行转账问题(A转账给B):A先减100,B再加100
  • i++不是原子性的
  • 用synchronized实现原子性

6.2、Java中的原子操作有哪些?

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

6.3、long和double的原子性

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

6.4、原子操作+原子操作!=原子操作

  • 简单地把原子操作组合在一起,并不能保证整体依然具有原子性
  • 全同步的HashMap也不能完全安全(多个synchronized方法操作组合在一起,就不是原子的了)

7、面试常见问题

7.1 JMM应用实例:单例模式8种写法、单例和并发的关系(真实面试超高频考点)

(1)单例模式的作用和使用场景

单例模式的作用

  • 为什么需要单例模式:节省内存和计算、保证结果正确、方便管理

单例模式适用场景

  • 无状态的工具类:比如日志工具类,不管是在哪里适用,我们需要的只是它帮我们记录日志信息,除此之外,并不需要在它的实例对象上存储任何状态,这个时候我们就只需要一个实例对象即可
  • 全局信息类:比如我们在一个类上记录网站的访问次数,我们不希望有的访问被记录在对象A上,有的却记录在对象B上,这时候我们就让这个类成为单例

(2)单例模式的8种写法

1、饿汉式(静态常量)[可用]

/**
 * Singleton1
 *
 * @author venlenter
 * @Description: 饿汉式(静态常量)(可用)
 * @since unknown, 2020-06-02
 */
public class Singleton1 {
    //类加载时就完成了初始化
    private final static Singleton1 INSTANCE = new Singleton1();
    private Singleton1() {
        
    }
    public static Singleton1 getInstance() {
        return INSTANCE;
    }
}

2、饿汉式(静态代码块)[可用]

/**
 * Singleton2
 *
 * @author venlenter
 * @Description: 饿汉式(静态代码块)(可用)
 * @since unknown, 2020-06-02
 */
public class Singleton2 {
    //类加载时就完成了初始化
    private final static Singleton2 INSTANCE;
    static {
        INSTANCE = new Singleton2();
    }
    private Singleton2() {

    }
    public static Singleton2 getInstance() {
        return INSTANCE;
    }
}

3、懒汉式(线程不安全)[不可用]

/**
 * Singleton3
 *
 * @author venlenter
 * @Description: 懒汉式(线程不安全)
 * @since unknown, 2020-06-03
 */
public class Singleton3 {
    private static Singleton3 instance;

    private Singleton3() {

    }

    public static Singleton3 getInstance() {
        //如果2个线程同时执行到这一行,则会执行2次new Singleton3(),创建了多个实例
        if (instance == null) {
            instance = new Singleton3();
        }
        return instance;
    }
}

4、懒汉式(线程安全,同步方法)[不推荐用]

/**
 * Singleton4
 *
 * @author venlenter
 * @Description: 懒汉式(线程安全)(不推荐)
 * @since unknown, 2020-06-03
 */
public class Singleton4 {
    private static Singleton4 instance;

    private Singleton4() {

    }

    //synchronized,多个线程执行会阻塞等待
    public synchronized static Singleton4 getInstance() {
        if (instance == null) {
            instance = new Singleton4();
        }
        return instance;
    }
}

5、懒汉式(线程不安全,同步代码块)[不可用]

/**
 * Singleton5
 *
 * @author venlenter
 * @Description: 懒汉式(线程不安全,同步代码块)(不可用)
 * @since unknown, 2020-06-03
 */
public class Singleton5 {
    private static Singleton5 instance;

    private Singleton5() {

    }

    public static Singleton5 getInstance() {
        //2个线程同时进入这里,则A线程synchronized执行完后,B线程又执行一次synchronized初始化
        //本质还是执行了2次初始化,线程不安全
        if (instance == null) {
            synchronized (Singleton5.class) {
                instance = new Singleton5();
            }
        }
        return instance;
    }
}

6、双重检查[推荐用]

/**
 * Singleton6
 *
 * @author venlenter
 * @Description: 双重检查(推荐使用)
 * @since unknown, 2020-06-03
 */
public class Singleton6 {
    private volatile static Singleton6 instance;

    private Singleton6() {

    }

    public static Singleton6 getInstance() {
        if (instance == null) {
            synchronized (Singleton6.class) {
                if (instance == null) {
                    instance = new Singleton6();
                }
            }
        }
        return instance;
    }
}
  • 优点:线程安全;延迟加载;效率较高
  • 为什么要double-check
①线程安全
②单check行不行?:不行多个线程同时执行到instance==null,虽然有synchronized,但也会执行了2次初始化
③直接在方法加synchronized呢?:性能问题,多个线程排队等待
  • 为什么要用volatile
①新建对象实际上有3个步骤:创建空对象、空对象内初始化、赋值
②重排序会带来nullpointexception
③防止重排序

7、静态内部类[推荐使用]

/**
 * Singleton7
 *
 * @author venlenter
 * @Description: 静态内部类方式,可用
 * @since unknown, 2020-06-03
 */
public class Singleton7 {

    private Singleton7() {

    }
    //JVM加载Singleton7类的时候,不会初始化内部类变量,达到了懒加载
    private static class SingletonInstance {
        private static final Singleton7 instance = new Singleton7();
    }

    public static Singleton7 getInstance() {
        //只有当调用到的是,才会进行加载
        return SingletonInstance.instance;
    }
}

8、枚举[推荐用]

/**
 * Singleton8
 *
 * @author venlenter
 * @Description: 枚举单例
 * @since unknown, 2020-06-04
 */
public enum  Singleton8 {
    INSTANCE;
    public void whatever() {

    }
}

//调用
Singleton8.INSTANCE.whatever();

(3)不同写法对比

  • 饿汉:简单,但是没有lazy loading,直接就初始化创建了一些对象,而这些对象可能是不需要的
  • 懒汉:写法复杂,同时有线程安全问题
  • 静态内部类:可用
  • 双重检查:同时做到了线程安全和懒加载
  • 枚举:最好

(4)用哪种单例的实现方案最好?

《Effective Java》中表明:使用枚举实现单例的方法虽然还没有广泛采用,但单元素的枚举类型已经成为实现Singleton的最佳方法

  • 写法简单
  • 线程安全有保障
  • 避免反序列化破坏单例

(5)各种写法的适用场景

  • 最好的方法是利用【枚举】,因为还可以防止反序列化重新创建新的对象
  • 非线程同步的方法不能使用
  • 如果程序一开始要加载的资源太多,那么就应该使用【懒加载】
  • 饿汉式如果是对象的创建需要配置文件就不适用(假设对象的创建需要调用一个前置方法去获取配置,但因为饿汉式,对象被提前创建,而没有将对应的前置方法数据赋值进去,造成创建的对象是一个空对象)
  • 懒加载虽然好,但是静态内部类这种方式会引入编程复杂性

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

  • JMM是什么?:一组规范
  • 最重要的3点内容:重排序、可见性、原子性
  • 可见性内容从主内存和本地内存、Happens-before原则、volatile
  • 原子性:实现原子性的方法、单例模式

7.3 volatile和synchronized的异同?

  • volatile可以算是轻量版的synchronized,开销小,适用场合相对就少一点:如果一个共享变量至始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替

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

  • 什么是原子操作:要么全部执行,要么全部不执行
  • Java中有哪里原子操作
除long和double之外的【基本类型】(int,byte,boolean,short,char,float)的赋值操作
所有引用【reference的赋值操作】,不管是32位的机器还是64位的机器
java.concurrent.Atomic.* 包中所有类的原子操作
  • 生成对象的过程是不是原子操作:是多步操作,无法保证原子操作
①新建一个空的Person对象
②执行Person的构造函数
③把这个对象的地址指向p

7.5 什么是内存可见性?

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

  • 32位上不是原子的,64位上是原子的,一般不需要我们考虑

8、总结:Java内存模型————底层原理

  • 什么叫“底层原理”
  • 三兄弟:JVM内存结构 VS Java内存模型 VS Java对象模型
  • JMM是什么
  • 重排序
  • 可见性
  • 原子性

笔记来源:慕课网悟空老师视频《Java并发核心知识体系精讲》

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Venlenter

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值