【Java并发底层原理精讲】JAVA内存模型-底层原理

1. 到底什么叫"底层原理"?本章研究的内容是什么?

1.1 重要性

Java 面试的必考知识点。只有学会了这一章的内容,才能说你真正懂了并发。

1.2 从JAVA代码到CPU指令

在这里插入图片描述

1.最开始,我们编写的Java代码,是*.java文件

2.在编译( javac命令)后,从刚才的*.java文件会变出一个新的Java字节码文件( *.class)

3.JVM会执行刚才生成的字节码文件( *.class ) , 并把字节码文件转化为机器指令

4.机器指令可以直接在CPU上运行,也就是最终的程序执行

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

1.4 重点开始向下转移

转化过程的规范、原则

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

2.1 整体方向

2.1.1 JVM内存结构:和JAVA虚拟机的运行时区域有关

在这里插入图片描述

堆(Heap):通过new或者其它方式创建的实例对象(包括数组)。如果这些对象不再被引用会被垃圾回收。的优势就是在运行时动态分配。

Java栈(VM stack):又称虚拟机栈,保存了各个基本数据类型,以及对于对象的引用。Java堆在编译时会确定大小,在运行时大小不会改变。

方法区(method):存储的是已经加载的各个static静态变量、类信息已经常量信息,还包含永久引用。

本地方法栈:保存的是和本地方法相关的信息;本地方法主要指native方法。

程序计数器:占用的区域是最小的,保存的是当前线程执行字节码的行号数,在上下文切换时,这个数据会被保存下来,包括需要下一条执行的指令、循环等异常处理。

2.1.2 Java内存模型,和Java的并发编程有关

2.1.3 Java对象模型,和Java对象在虚拟机中的表现形式有关,是对对象的抽象。

在这里插入图片描述

Java对象自身的存储模型

JVM会给这 个类创建-个instanceKlass ,保存在方法区,用来在JVM层表示该Java类。

当我们在Java代码中,使用new创建一个对象的时候 , JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据

3. JVM是什么

Java Memory Model(JVM内存模型)

  • C语言不存在内存模型的概念。

  • 依赖处理器,不同的处理器结果不一样

  • 无法保证并发安全

  • 需要一个标准,让多线程的运行结果可预期

  • 规范

3.1 为什么需要JMM

需要各个JVM的实现来遵守JMM规范,以便开发者可以利用这些规范,更方便地开发多线程程序

如果没有这样的一个JMM内存模型来规范,那么很可能经过不同JVM的不同规则的重排序之后,导致不同的虚拟机上的运行结果不一样,那是很大的问题。

  • 是工具类和关键字的原理

volatile、synchronized、Lock等的原理都是JMM

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

  • 最重要的3点内容:重排序、可见性、原子性

4. 重排序

4.1 重排序的代码案例、什么是重排序

/**
 * 演示重排序的现象
 * */
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 thread1 = new Thread(()->{
           a = 1;
           x = b;
        });
        Thread thread2 = new Thread(()->{
            b = 1;
            y = a;
        });
        
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println( "x = " + x +",y = " + y);
    }
}

程序输出结果

x = 1,y = 0

Process finished with exit code 0

程序结果分析

赋值操作的4行代码的执行顺序决定了最终x和y的结果,一共有3总情况:

  • thread1先运行,a=1,x=b(0);b=1,y=a(1);,最终结果是x=0,y=1;
  • thread2先运行,b=1,y=a(0);a=1,x=b(1);,最终结果是x=1,y=0;
  • b=1;a=1,x=b(1);x=a(1),y=b(1);,最终结果是x=1,y=1;
/**
 * 演示重排序的现象
 * "直到到达某个条件才停止",测试小概率事件
 * */
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 count  =0;
       for (;;){
           count++;
           a = 0;
           b = 0;
           x = 0;
           y = 0;
           CountDownLatch latch = new CountDownLatch(1);
           Thread thread1 = new Thread(()->{
               try {
                   latch.await();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               a = 1;
               x = b;
           });
           Thread thread2 = new Thread(()->{
               try {
                   latch.await();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               b = 1;
               y = a;
           });

           thread2.start();
           thread1.start();
           latch.countDown();
           thread1.join();
           thread2.join();
           System.out.println("当前运行" + count + "次," + "x = " + x + ",y = " +y);
           if(x == 1 && y ==1){
               break;
           }

       }
    }
}
虽然代码执行顺序可能有很多种情况,但在线程1内部,也就是
a = 1;
x = b;
按照刚才的运行结果这两行代码的执行顺序是不会改变的,也就是a=1会在x=b前执行,同理,现在2的b=1;会在y=a;之前执行。因此无论如何也不会出现x=;y=0;的情况,但是真实情况会如此吗?
/**
 * 演示重排序的现象
 * "直到到达某个条件才停止",测试小概率事件
 * */
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 count  =0;
       for (;;){
           count++;
           a = 0;
           b = 0;
           x = 0;
           y = 0;
           CountDownLatch latch = new CountDownLatch(1);
           Thread thread1 = new Thread(()->{
               try {
                   latch.await();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               a = 1;
               x = b;
           });
           Thread thread2 = new Thread(()->{
               try {
                   latch.await();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               b = 1;
               y = a;
           });

           thread2.start();
           thread1.start();
           latch.countDown();
           thread1.join();
           thread2.join();
           System.out.println("当前运行" + count + "次," + "x = " + x + ",y = " +y);
           if(x == 0 && y ==0){
               break;
           }

       }
    }
}

程序输出结果

...
当前运行954,x = 1,y = 0
当前运行955,x = 1,y = 0
当前运行956,x = 0,y = 0

Process finished with exit code 0

程序结果分析

会出现x=0,y=0?那是因为发生了重排序,4行代码的执行顺序的其中一种可能是:

y=a;
a=1;
x=b;
b=1;

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

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

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

在这里插入图片描述

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

  • 编译器优化

编译器(包括JVM,JIT编译器等)出于优化的目的(例如当前有了数据a,那么如果把对a的操作放到一起效率会更高,避免了读取b后又返回来重新读取a的时间开销),在编译的过程中会进行一定程度的重排,导致生成的机器指令和之前的字节码的顺序不一致。
在刚才的例子中,编译器将y=a和b=1这两行语句换了顺序(也可能是线程2的两行换了顺序,同理),因为它们之间没有数据依赖关系,那就不难得到 x =0,y = 0 这种结果了。

  • CPU指令重排序

CPU 的优化行为,和编译器优化很类似,是通过乱序执行的技术,来提高执行效率。所以就算编译器不发生重排,CPU 也可能对指令进行重排,所以我们开发中,一定要考虑到重排序带来的后果。

  • 内存的"重排序:"线程A的修改线程B却看不到,引出可见性问题

内存系统内不存在重排序,但是内存会带来看上去和重排序一样的效果,所以这里的“重排序”打了双引号。由于内存有缓存的存在,在JMM里表现为主存和本地内存,由于主存和本地内存的不一致,会使得程序表现出乱序的行为。
在刚才的例子中,假设没编译器重排和指令重排,但是如果发生了内存缓存不一致,也可能导致同样的情况:线程1 修改了 a 的值,但是修改后并没有写回主存,所以线程2是看不到刚才线程1对a的修改的,所以线程2看到a还是等于0。同理,线程2对b的赋值操作也可能由于没及时写回主存,导致线程1看不到刚才线程2的修改。

5. 可见性

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


public class FieldVisible {
    int a = 1;
    int b = 2;

    public static void main(String[] args) {
        while (true){

            FieldVisible fieldVisible = new FieldVisible();
            System.out.println(fieldVisible.a + " " + fieldVisible.b);
            new Thread(()->{
                fieldVisible.change();
            }).start();
            new Thread(()->{
                fieldVisible.print();
            }).start();
        }

    }

    private void change() {
        a = 3;
        b = a;
    }
    private void print(){
        System.out.println("a = " + a +",b = " +b);
    }
}

程序结果分析

无论程序如何执行,都只会出现以下4种执行结果:

a = 3; b = 3;
a = 1; b = 2;
a = 3; b = 2;
a = 1; b = 3;

5.2 为什么会有可见性问题

在这里插入图片描述

从图中可以发现,每个线程都有自己的工作内存,工作内存中存的是主内存共享变量的副本。线程如果想要得到共享变量的值,只能先把相应的主内存变量读到自己的工作内存中,然后再从工作内存中读取,修改之后写回工作内存,最后再同步回主内存。而各个线程间的工作内存是不共享的,所以如果线程一修改了某个变量的值,线程二想要看到,就得等线程一把变量同步回主内存中,才能看到这次变化,读取到最新的值,否则读取的话很可能就是过期的变量值,也就引发了多线程的可见性问题。
————————————————
版权声明:本文为CSDN博主「绅士jiejie」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_38106322/article/details/105745555

在这里插入图片描述

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

高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在CPU和主内存之间就多了Cache层。

线程间的对于共享变量的可见性问题不是直接由多核引起的,而是由多缓存引起的。

如果所有核心都只用一个缓存,那么也就不存在内存可见性问题了。

每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存中。所以会导致有些核心读取的值是一个过期的值。

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

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

Java作为高级语言,屏蔽了这些底层细节,用JMM定义了一套读写内存数据的规范,虽然我们不需要再关心一级缓存和二级缓存的问题,但是JMM抽象了本地内存和主内存的概念。

这里说的本地内存并不是真的是以快给每个线程分配的内存,而是JMM的一个抽象,是对于寄存器、一级缓存、二级缓存等的抽象

在这里插入图片描述
在这里插入图片描述

5.3.2 主内存和本地内存的关系是什么?

JMM有以下规定:

所有的变量都存储在主内存中,同时每个线程也有自己独立工作内存,工作内存中的变量内容是主内存中的拷贝

线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后再同步到主内存中。

主内存多个线程共享的,但线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转来完成。

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

5.4 Happens-Before原则

5.4.1 什么是happens-before

happens-before规则是用来解决可见性问题的:在时间上动作A发生在动作B之前,B保证能看见A,这就是happens-before。

两个操作可以用happens-before来确定它们的执行顺序:如果一个操作happens-before于另一个操作,那么我们说第一个操作对于第二个操作是可见的

5.4.2 什么不是happens-before

两个线程没有相互配合的机制,所以代码X和Y的执行结果并不能保证总被对方看到的,这就具备happens-before。

5.4.3 Happens-Before规则有哪些?

5.4.3.1 单线程规则

在单线程内,后面的语句一定能看到前面的语句。Happens-Before并不影响重排序。

5.4.3.2 锁操作(synchronized和Lock)

如果一个线程对lock解锁,另一个线程对lock加锁,加锁之后的线程一定能看到解锁线程的操作。

5.4.3.3 volatile变量

在这里插入图片描述

A线程对volatile修饰的共享变量做了修改一定对B线程可见。

5.4.3.4 线程启动

在这里插入图片描述

子线程执行的所有语句都能看到主线程之前的所有发生操作。

5.4.3.5 线程join

在这里插入图片描述

一旦使用join()join()后面的语句一定能看到join()前面的语句。

5.4.3.6 传递性

如果h吧(A,B)而且hb(B,C),那么可以推出hb(A,C)

5.4.3.7 中断

一个线程被其它线程interrupt,那么检测中断(isInterrupted)或者抛出InterruptedException一定能看到。

5.4.3.8 构造方法

对象构造方法的最后一行指令happends-before于finalize() 方法的第一行指令。

5.4.3.9 工具类的Happens-Before原则

线程安全的容器get一定能看到在此之前的put等存入动作

CountDownLatch

Semaphore

Future:get()方法

线程池:可以看到在submit()之前的所有执行结果。
CyclicBarrier

案例:happens-before演示
public class FieldVisible {
    int a = 1;
    volatile int b = 2;

    public static void main(String[] args) {
        while (true){

            FieldVisible fieldVisible = new FieldVisible();
            System.out.println(fieldVisible.a + " " + fieldVisible.b);
            new Thread(()-> fieldVisible.change()).start();
            new Thread(()-> fieldVisible.print()).start();
        }

    }

    private void change() {
        a = 3;
        b = a;
    }
    private void print(){
        System.out.println("a = " + a +",b = " +b);
    }
}

近朱者赤:给b加了valotaile,不仅b被影响,也可以实现轻量级同步

b 之前的写入(对应代码b=a)对读取b后的代码(print b)都可见,所以在writerThread里对a的赋值,一定会对readerThread里的读取可见,所以这里的a即使不加volatile,只要b读到3,就可以由happens-before原则保证了读取到的都是3而不可能读取到1。

5.5 volatile关键字

5.5.1 volatile是什么

volatile是一种同步机制,比synchronized或者Lock相关类更轻量,因为使用volatile不会发生上下文切换等开销很大的行为。

如果一个变量被修饰成volatile,那么JVM就知道了这个变量可能会被并发修改

但是开销小,相应的能力也小,虽然说volatile是用来同步保证线程安全的,但是volatile做不到像syncheonized那样的原子保护,volatile仅在很有限的场景下才能发挥作用。

5.5.2 volatile的适用场合

不适用:a++

import java.util.concurrent.atomic.AtomicInteger;

public class NoVolatile implements Runnable{
    volatile int a = 0;
    AtomicInteger realA = new AtomicInteger(0);
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            a++;
            realA.incrementAndGet();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        NoVolatile noVolatile = new NoVolatile();
        Thread thread1 = new Thread(noVolatile);
        Thread thread2 = new Thread(noVolatile);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("获取到a=" + noVolatile.a);
        System.out.println("真实累加次数" + noVolatile.realA.get());
    }
}

程序输出结果

获取到a=17961
真实累加次数20000

Process finished with exit code 0

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

使用场景2:作为刷新之前变量的触发器

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

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

//Thread B
while(!initialized){
    sleep();
}

//use configOptions

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

可见性:读一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个volatile属性会立即刷入到主内存

禁止指令重排序优化:解决单例双重锁乱序问题。

volatile和synchronized的关系?

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

用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 count  =0;
       for (;;){
           count++;
           a = 0;
           b = 0;
           x = 0;
           y = 0;
           CountDownLatch latch = new CountDownLatch(1);
           Thread thread1 = new Thread(()->{
               try {
                   latch.await();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               a = 1;
               x = b;
           });
           Thread thread2 = new Thread(()->{
               try {
                   latch.await();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               b = 1;
               y = a;
           });

           thread2.start();
           thread1.start();
           latch.countDown();
           thread1.join();
           thread2.join();
           System.out.println("当前运行" + count + "次," + "("+ x+"," +y+")");
           if(x == 0 && y ==0){
               break;
           }

       }
    }
}

volatile小结

volatile修饰符适用于以下场景: 某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如boolean flag;或者作为触发器,实现轻量级同步。

volatile 属性的读写操作都是无锁的,它不能替代synchronized ,因为它没有提供原子性互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。

volatile只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序

volatile 提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile 属性不会被线程缓存,始终从主存中读取

volatile提供了happens-before保证,对volatile变量v的写入happens-before所有其他线程后续对v的读操作。

volatile可以使得long和double的赋值是原子的

讲long和double的原子性。

5.6 保证可见性的措施

除了volatile可以让变量保证可见性外,synchronized、Lock、并发集合、Thread.join()和Thread.start()等都可以保证可见性。

具体看happens-before原则的规定

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

synchronized不仅保证了原子性,还保证了可见性

synchronized不仅让被保护的代码安全,还近朱者赤

public class FieldVisible {
    int a = 1;
    int ab = 2;
    int abc = 3;
    int abcd = 4;

    public static void main(String[] args) {
        while (true){

            FieldVisible fieldVisible = new FieldVisible();
            new Thread(()-> fieldVisible.change()).start();
            new Thread(()-> fieldVisible.print()).start();
        }

    }

    private void change() {
        a = 5;
        ab = 6;
        abc = 7;

        synchronized (this){
            abcd = 8;
        }
    }
    private void print(){
        synchronized (this){
            int q = a;
        }
        int w = ab;
        int e = abc;
        int r = abcd;

    }
}

6. 原子性

6.1 什么是原子性

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

ATM取钱

i++不是原子性的。
synchronized实现原子性

6.2 Java中的原子操作有哪些

除了long和double之外的基本类型(int,byte,boolean,short,char,float)的赋值操作。

所有引用reference的赋值操作,不管是32位还是64位的机器

java.concurrent.Atomic.*包中所有类的原子操作

6.3 longdouble的原子性

Non-Atomic Treatment of double and long

问题描述:官方文档、对于64位的值的写入、可以分为两个32位的操作进行写入、读取错误、使用volatile解决

结论:在32位的JVM上,long和double的操作不是原子的,但是在64位的JVM上是原子的。

实际开发中:商用Java虚拟机中不会出现

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

简单地把原子操作组合在一起,并不能保证整体依然具有原子性。

全同步的HashMap也不完全安全:Collections.synchronizedMap(new HashMap<>());每个方法是同步的,但是组合在一起在多线程的情况下就可能出错。

7. 常见面试问题总结

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

单例模式的作用

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

单例模式适用场景

无状态的工具类:比如日志工具类,不管在那里使用,我们要的只是它帮我们记录日志信息,除此之外,并不需要在它的实例对象上存储任何状态,这时候我们就只需要一个实例对象即可。

全局信息类:比如我们在一个类上记录网站的访问次数,我们不希望有的访问记录在对象A上,有的却记录在对象B上,这时候我们就让这个类称为单例。

单例模式的8种写法

饿汉式(静态常量)[可用]
/**
 *饿汉式(静态常量)[可用]
 * 优点:1.简单;2.类装载的时候就完成了实例化。
 * */
public class Singleton1 {

    private static final Singleton1 INSANCE = new Singleton1();

    private Singleton1() {
    }

    public static Singleton1 getInstance(){
        return INSANCE;
    }
}
饿汉式(静态代码块)[可用]
/**
 *饿汉式(静态代码块)[可用]
 * 优点:1.简单;2.类装载的时候就完成了实例化。
 * */
public class Singleton2 {

    private static final Singleton2 INSANCE;
    static {
        INSANCE = new Singleton2();
    }

    private Singleton2() {
    }

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

懒汉式(线程不安全)[不可用]
/**
 * 懒汉式(不可用)多线程时,会导致创建多个实例
 * */
public class Singleton3 {
    
    private static Singleton3 INSTANCE;
    
    private Singleton3(){
        
    }
    public static Singleton3 getInstance(){
        if(INSTANCE == null){
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }
}

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

/**
 * 懒汉式(线程安全,同步方法)[不推荐用]
 * 缺点:效率太低
 * */
public class Singleton4 {

    private static Singleton4 INSTANCE;

    private Singleton4(){

    }
    public synchronized static Singleton4 getInstance(){
        if(INSTANCE == null){
            INSTANCE = new Singleton4();
        }
        return INSTANCE;
    }
}

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

/**
 * 懒汉式(线程不安全,同步代码块)[不可用]
 * 缺点:两个线程都能进入if代码块,一个线程执行完同步代码块时,另一个线程依旧会执行同步代码块,从而创建多个对象
 * */
public class Singleton5 {

    private static Singleton5 INSTANCE;

    private Singleton5(){

    }
    public static Singleton5 getInstance(){
        if(INSTANCE == null){
            synchronized(Singleton5.class){
                INSTANCE = new Singleton5();
            }
        }
        return INSTANCE;
    }
}

双重检查[推荐用]

/**
 *双重检查(推荐面试使用)
 *    优点:线程安全;延迟加载;效率较高;
 *    为什么要double-check
 *        1.线程安全
 *        2.单check行不行?
 *            不行,线程不安全(为什么不安全)
 *        3.直接把synchronized加到方法上不行么?
 *            可以的,线程安全的,但是有很大的性能问题,当多个线程访问的时候,不能及时响应。
 *    为什么要用volatile? (CPU重排序)
 *        1.新建对象实际上有三个步骤
 *        2.重排序会带来NPE问题
 *        3.防止重排序
 *
 * */
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;
    }
}

在这里的双重检查想要防止的,是这种特殊情况:
“在第一个线程退出synchronized之前,里面的操作执行了一部分,比如执行了new却还没执行构造函数,然后第一个线程被切换走了,这个时候第二个线程刚刚到第一重检查,所以看到的对象就是非空,就跳过了整个synchronized代码块,获取到了这个单例对象,但是使用其中的属性的时候却不是想要的值。”

在这里插入图片描述
The “Double-Checked Locking is Broken” Declaration

双重检查模式中volatile起到的作用

静态内部类[推荐用]

/**
 * 静态内部类 可用(属懒汉)
 * 优点:线程安全;懒加载
 */
public class Singleton7 {

    private Singleton7(){

    }
    
    private static class SingletonInstance{
        private static final Singleton7 INSTANCE = new Singleton7();
        
    }
    public static Singleton7 getInstance(){
        return SingletonInstance.INSTANCE;
    }
    
}

枚举[推荐用]


/**
 * 枚举单例
 * */
public enum Singleton8 {
    INSTANCE;
    
    public void method1(){
        
    }
}

不同写法对比

饿汉:简单,但是没有lazy loading

懒汉: 有线程安全问题

静态内部类:可用

双重检查:面试用(同时做到线程安全和懒加载)

枚举:最好

Joshua Bloch在《Effective Java》中明确表达过的观点:“使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。”

写法简单

线程安全有保障:枚举是一种特殊的类,不需要我们保证代码安全,经过反编译,枚举会被
编译成finalclass,继承了枚举这个父类,并且在这个父类中它的各个实例都是通过static来定义的。枚举的本质是静态的对象。

避免反序列化破坏单例:防止反序列化创建新的对象。

非线程安全的方式不能使用

如果程序一开始要加载的资源太多,那么就应该使用懒加载

饿汉式如果是对象的创建需要配置文件就不适用。

懒加载虽然好,但是静态内部类这种方式会引入编程复杂性。

单例模式面试常见问题

饿汉式的缺点?

懒汉式的缺点?

为什么要用double-check?不用就不安全么?

为什么双重检查模式要用volatile?

面试常见问题

JMM应用实例:单例模式8种写法、单例和并发的关系

讲一讲什么是Java内存模型

volatile和synchronized的异同

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值