前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >内功修炼-击败JMM内存模型

内功修炼-击败JMM内存模型

原创
作者头像
Joseph_青椒
修改2023-08-18 21:18:02
4090
修改2023-08-18 21:18:02
举报
文章被收录于专栏:java_josephjava_joseph

文章初衷:

这篇文章,也是在解决我在java学习上的一些疑惑,堆、栈、堆栈,以及方法区、jvm虚拟机栈、本地方法栈,对于大学生的我,很是头疼,在学习jvm时候,学习到 jvm 结构模型,然后和之前看的JMM Java对象模型,是有区别的,有一次,碰到一个java是值传递还是引用传递,也用到了堆栈,总之很重要,想要对很多java知识的彻底理解,JMM是很重要的,然而大多是资料,怎么说,不讲人话,我是学一半就放弃,于是乎,就写本篇博客,一是方便自己复习巩固,二是帮助和我一样的小白,减少踩坑。文章可能有些长,但是一定很值得研究!

底层原理

这里主要讲述,并发底层到底什么,怎么实现的?

从java代码到cpu指令

Demo.java---javac编译--->Demo.class(字节码)------java运行---->JVM

字节码,byteCode,平台无关性

JVM会把字节码翻译成不同平台的cpu指令,例如windows、linux等等

而不同的cpu机器指令千差万别,无法保证并发安全的效果是一致的,JMM内存模型就诞生了!

通过JMM保证java代码最终落地,实现的是一样的

三兄弟区分

JVM内存结构 VS Java内存模型 VS Java对象模型

上面说到,因为java的平台无关性,衍生的问题,出现了java内存模型,这就使得java独特的内存模型,出现很多误导,我看各种文档,也是十分的混淆,怎么说,就是误人子弟!浪费生命,所以有必要细致讲一下这里的区别

JVM内存结构,和java虚拟机的运行时区域有关,可以理解是java代码存放的jvm中,的内存分离

Java内存模型,记住和java的并发编程相关就可以了

Java对象模型,指的是对象在虚拟机中的表现形式

jvm内存结构

java代码是运行在jvm虚拟机,虚拟机会把运行过程中的内存分为不同的区域,方便管理

运行时数据区,分为下面五部分,一提到jvm内存结构,我们脑子里第一个词是运行时数据区分为5个部分,知道内存结构式什么

image-20230817154427436
image-20230817154427436

这是我jvm专栏里的一张图,这里重点说一下,元数据区,也就是方法区、和堆,是线程共享的,其他都是线程私有的

堆Heap存放的对象,各种对象、以及数组,GC会回收

虚拟机栈(VM stack)

保存了各种基本数据类型,以及对于对象的引用,特点是:编译的时候,虚拟机栈的大小就已经确定了,并且运行的时候,大小不会改变,

方法区:

存储以及加载的static静态变量,类信息,常量信息,包含永久引用,比如static修饰一个对象,对象在新版jdk中,也是放在堆中的,这个引用就是永久引用,不会随线程的消逝而影响

本地方法栈:

native方法相关的栈

程序计数器:

保存当前字节码文件执行的行号数,还包含下一条需要执行的指令、异常处理等

这就是jvm内存模型

java对象模型

java是面向对象的,所以每一个对象在jvm存储是有一定结构的,这个结构,指的就是java对象模型

可以说java对象模型是依托jvm内存结构的,专门指java对象如何在jvm存储,以及它的结构,如下图:

image-20230817160227742
image-20230817160227742

一个类的诞生,会在方法区定义类的一些信息jvm给这个类创建一个instanceKlass保存在方法区,当

被创建对象时,对象会在堆中出现,堆中对象的结构包括对象头(MarkWord+元数据指针,指向方法去类的信息) 和实例数据,也就是类中定义的数据

,对象被某一个方法调用,就会在栈中把引用保存下来

所以java对象模型是栈、堆、方法区构成的

JMM

why 需要JMM

JMM---Java Momory Model java内存模型

C、C++语言,不存在内存模型概念,

依赖处理器,不同的处理器处理的结果不一样!,无法保证并发安全

假设java没有JMM内存模型,把代码用synchronized保护起来,不同平台处理,还是可能有问题的,所以就需要一个标准,让多线程运行的结果可预期,通过jmm的规范,

规范

没错JMM就是一个规范,让java语言,jvm、编译器、cpu利用这个规范,让咱顶层开发不用考虑很多细节

比如:jvm有各种实现比如openJDK,让他们解释代码、重排序要遵守这个规范,以免不同虚拟机导致执行的结果不一样,

JMM是工具类、关键字的原理

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

JMM的出现,让我们使用这些关键字就可以开发并发的程序,不需要自己注意内存栅栏的开发

JMM最最主要内容

知道JMM干什么的了

那么重点是什么,

一提到JMM,我们马上就需要说出来,重排序、可见性、原子性,这是JMM最主要内容,可以说,JMM就是重排序+可见性+原子性

重排序

代码案例

代码语言:javascript
复制
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;
                }
            });
            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);
            }
        }
    }
​
​
}
image-20230817181634381
image-20230817181634381

这3个条件,就是正常不发生重排序的可能,也就是默认线程里执行的顺序都是一样的,有三种情况

那么00这个也是会发生的

image-20230817181945386
image-20230817181945386

这是为何?

y=a \ a=1\x=b\b=1这种情况,不按照顺序执行,会出现上面运行的情况,这就是重排序

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

image-20230817182606007
image-20230817182606007

看这个例子,9条指令,重排序后,简化为7条,一条没什么,多了呢?就会明显提高速度

重排序的3种情况

编译器优化:

包括jvm、JIT编译器等,

常见的是指令没有依赖关系的情况,会认为没有依赖关系,会默认进行指令重排

cpu指令重排

编译器不指令重排,cpu也可能进行指令重排

内存的“重排序”

这里的重排序并不是真正的重排序,表面现象的重排序

因为内存中有缓存存在,在JMM中,表现为主存和本地内存,他们不一定保持一致,这就导致A线程的修改B线程看不到,就会出现和重排序一样的现象,

这里也引出了可见性问题

可见性

案例演示

代码语言:javascript
复制
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(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.change();
                }
            }).start();
​
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
​
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.print();
                }
            }).start();
        }
    }
​
​
}

分析段代码可能出现的结果

a=3 b=3 先运行change线程

a=1 b=2 先运行print线程

a=3 b=2 change运行一个a=3,然后print

but,上面代码运行还有一种情况的出现

假设第一个线程change了,a = 3 b=3,但是第二个线程只看到了b 的修改,没看到a的修改,就会出现 a=1 b=3的情况

为什么会有可见性问题?

we know,cpu多核时代,每个核有自己的内存,他们有一个共享的内存,通过共享的内存交互,就出现了延迟的现象,也就是可见性问题的由来,也就是八股中常看到的,主存和本地内存

总结就是:CPU有多级缓存,读到了过期 的旧数据

当然这是一个简单的图,学过计算机组成、或者操作系统的伙伴,一定知道,cpu的多级缓存,多核cpu,一级,二级,三级缓存,还有主存,还有离核最近的寄存器,RAM主存,速度和成本,大家可以从网上看哈,这里只讲原理,因为多级缓存的提效,这就是为什么会有可见性问题,

这里也回到了刚开始说的为什么需要JMM的问题,依赖处理器,不同OS的缓存也是不一样的,这就需要一个JMM规范,来防止出现因为底层系统原因导致的同步可见性问题。

我们再看一个例子

代码语言:javascript
复制
public class FieldVisibility {
​
   int x = 0;
​
   public void wiriterThread(){
       x = 1;
   }
​
   public void readerThread(){
       int r2 = x; 
   }
​
}

如果不发生可见性问题,那么读处理的是1,发生则是0

image-20230817194802398
image-20230817194802398

当发生的时候,就会出现这样的问题,线程一写进去了,但是还没同步到主存,线程2,读的就是旧的数据0

救星volatile

当我们使用valatile修饰变量的时候,就不会有可见性的问题,保证主存写入,本地内存一定能读到最新的数据

image-20230817195622469
image-20230817195622469

他会让线程1,强制的flush到主内存中,就会读取到正确的值,r2读shard cache主存的时候,就会读到正确的值

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

什么叫做主内存和本地内存

上面说到了,操作系统层面的多级缓存,引发的可见性问题,但是java语言屏蔽了细节,我们只需要考虑,JMM内存模型抽象出来的主内存和本地内存的概念。并没有寄存器,一级缓存,二层缓存,所有的处理器,到这层首先上,只有两个,就是主内存和本地内存,底层的不需要考虑了,就是下面这张图

image-20230817201655622
image-20230817201655622

这样抽象成两次,既满足了开发,也满足了cpu的多级缓存,既开发方便了些,又没浪费cpu缓存的机制

主内存与本地内存的关系(很重要)

这里很重要!!!!!!!!!!!!!!!!!

JMM是这样规定他们的关系的

1:所有的变量都存储在主存中,每个线程有自己独立的工作内存,工作内存中的变量都是对主存中的拷贝。主体都是在主内存中的

2:线程不能直接读写主内存变量,只能操作自己工作内存,再同步主内存

3:主内存是多个线程共享的,线程之间是不共享的,线程通信需要通过主内存通信

总结一下:变量是存在主内存的,每个线程有自己的工作内存,但是线程的交互需要通过主内存中转,这也是为什么会出现可见性问题

Happens-Before原则

什么不是happens-Before

先说什么不是,便于理解,

第一个线程,执行一个东西,线程B有时候看的到,有时候看不到,说明不具备happens-before,就是上面change、print、未加volitaie关键字的例子

什么是happends-Before

一个操作发生只后,对另一个操作一定是可见的,这就是happends-Before,重点是可见性的问题

Happends-Before规则有哪些

这里不一定能记住哈,哈哈,但是了解是很有必要的

1、单线程原则

我们讲到,本地内存,也就是工作内存,是有自己的内存的,那么对于单个线程,比如某个线程具有两条语句,第一个语句执行了,那么第二个语句一定是能看到的,这种在单个线程内语句执行的可见,就是单线程原则

image-20230817212709461
image-20230817212709461
代码语言:javascript
复制
private void change() {
    a = 3;
    b = a;
}

比如这个操作,某一行执行,那么另一行一定是可见的,为什么是某一行,不是第一行呢?对的,就是之前说的重排序问题

2、锁操作(自己关注)

synchronized 和 lock

image-20230818111425961
image-20230818111425961

这个就很简单了,线程A拿到锁,操作,那么线程B拿到锁的时候,是一定能看到的

3、volatile(自己关注)

volatile就是上面的例子,只要这个变量加了volatile变量,修改了之后 ,其他线程就一定能读到

4、线程启动

这里线程启动,默认是没有问题的,比如主线程,和子线程,子线程启动的时候,能看到主线程之前发生的事情

5、线程join

如何执行了join,那么join一下的代码一定能看到join线程之前的操作。

6、传递性

如果hb(A,B) hb(B,C) 那么可以推出hb(A,C)

7、中断

一个线程被其他线程interrupt,那么一定能检测到他们被中断的。

8、构造方法

构造方法的最后一条指令happens-before于finalize()方法的第一条指令

9工具类的Happens-Before

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

2、CountDownLatch(关注)

这里就是必须等执行完latch.countDown()之后,latch.await() 才能苏醒,保证闸门的正常

3、Semaphere(关注)

信号量,想要在信号量里面获取一个许可证,那么必须之前必须要有人释放,才能保证Semaphere正常

4、Future

future的get方法,get方法一定是可以拿到运行之后的数据

5、线程池

线程池的每个任务提交的时候,都可以看到之前运行的结果

6 、CyclicBarrier (关注)

也是栅栏的作用,必须要达到我释放的条件,不能随意的释放

他们都具有Happens-Before原则

这里这么多,我们只需要了解其他的,着重注意加锁和volatile这两个原则就可以了

详解volatile关键字

what is volatile

1、一种同步机制,只要用了voatile,那么它一定和同步相关,和多线程相关,代替锁和synchronized,更轻量级,不会发生上下文切换

2、如果修饰volatile,那么jvm就会知道这个变量会被并发修改,jvm就会注意它的并发问题,比如清理重排序

3、开销小,但是能力也小,虽然能满足线程同步,但是做不到像synchronized那样的原子同步。适用场景很局限

volatile适用场合

先说下不使用的场合:a++

代码语言:javascript
复制
public class NoVolatile implements Runnable{
​
    volatile int a;
    AtomicInteger readA = 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).readA.get());
​
    }
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            a++;
            readA.incrementAndGet();
        }
    }
}
结果:
    18402
    20000

适用场景:一个共享变量自始至终被只被其他线程去赋值,而不进行其他操作,比如自增,那么就可以用volatile代替synchronized

对布尔值赋值,boolean flag;

这也体现了两种的区别,直接赋值时原子操作,而a++这样的,是非原子操作,需要读a的值,+1,再赋值,这是非原子,而直接给flag赋值,是原子操作,就不会有这样的问题

但是注意!适用也只是对于只赋值的情况,只要是依赖于原来的值,做出改变,只要是非原子,都不适用于volatile的情况

所有总结一下:volatile保证了可见性,而赋值时原子性的,所有保证了线程安全

作为刷新之前变量的触发器

这里很简单,要知道,volatile符合happends-before,比如b变量,用volatile修饰,那么它之前的一些列没加volatile变量的赋值,只要执行到这里,不加volatile也是会变的可见的,由于volatile保证了b变量的happends-before,之前变量都会刷新到主存中。

这里还说明了,volatile不仅有益于当前变量,还会使得其他变量受益

看下图例子

image-20230818142355548
image-20230818142355548

这是项目种的一个例子,加载一些列配置,不同的线程,只要initialized为true了,那么就知道配置好了!,线程B就可以执行了,这样就保证了线程安全!

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

现在总结一下volatile的两个作用,第一个肯定就是可见性了,上面很多话都是在说这些

机制时这样的:修饰了volatile,读的时候,情况本地内存,只能从主存中读

写的时候,一定会同步到主内存,供其他线程读到

2:用了volatile的时候,会禁止重排序:

案例:解决单例双重锁乱序

volatile和synchronized的关系

volatile是轻量级的synchronized,它通过可见性,以及操作的原子性保证线程安全,缺点就是变量的操作必须是原子性的,

volatile还禁止了重排序

总结回顾

场景:适用于直接修改的原子操作,第二个是利用volatile happends - before的特性,实现触发器,保证之前的变量也可见,刷新之前变量

2:volatile是无锁的,不能保证原子性和互斥性,不能替代synchronized,成本低

3:只能用于某个属性,不像synchronized用于代码块和方法

4:就是上面场景说的,提供了可见性,以及提供了happneds-before的保障

还有个:就是volatile可以使得long、double的赋值也是原子性的,这个会在下面的原子性讲到,这里先点一下

保证可见性的措施

除了volatile让变量保证可见性

还有、synchronized、lock、并发集合、TThread.join Thread.start都可以保证可见性

也就是上面说的,happends-before原则的规定

思考:synchronized可见性

synchronized不仅保证了原子性,它还保证了可见性,为什么呢?

synchronized保证并发的时候,比如两个线程,都给一个i变量自增,每个线程自增1万次,run方法中,

image-20230818145621771
image-20230818145621771

i++;被同步代码块保证,那么肯定是原子性的,如果没有保障可见性,那么最终结果也不会是2w,所有synchronized也是保证了可见性的

第二点:

上面讲到,volatile可以让之前没加volatile变量也进行更新,做到可见,近朱者赤一样

synchornized也是可以的

image-20230818145904788
image-20230818145904788

比如这样,线程B不仅能看到线程一加锁的内容,也能看到线程A加锁之前的一些列内容,这都归功于synchronized对happens-before的实现,和上面volatile保证之前变量,场景是一样的

image-20230818151126432
image-20230818151126432

比如这个例子,第二个加锁的时候,会知道第一个锁的内容,以及第一个锁之前的内容。

到这里JMM的可见性就结束了,主要就是happends-before原则,重点就是synchronized lock锁、以及volatile这些。

值得考虑的是,happends不仅体现了volatile、synchronized修饰的地方,也保证了他们之前的数据的happends-before!近朱者赤!

原子性

什么是原子性

一些列操作,要不全部失败要么全部成功,是不可分割的

这里就要说明下并发编程和数据库中的A的区别

数据库中是事物执行,要么全部执行成功,要么回滚

而并发编程不支持回滚,他强调的是一个操作的不可分割线,就是一个线程执行的时候,会一次都执行完,不会被其他线程干扰。

image-20230818185144183
image-20230818185144183

比如i++操作,就不是原子性的,包括取数,加,赋值,有可能某个操作,就被其他线程干扰了。当然这是java的例子,这样上面的例子,就少加了一次,本来应该是2 ,却成为1,synchronized保护起来。就不会发生干扰,就实现了i++的原子性

java中的原子性

java语言中原子性,更注重说的是这个操作不可再分,一段代码、一个变量的操作,不会被其他线程执行、干扰

天然的原子性,不是很多

有这几种

除long和double之外的基本数据类型(short int boolean char float)

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

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

long和double 的原子性

image-20230818190412077
image-20230818190412077

看下官方语言怎么说的:

意思就是:

在32位的JVM中,long double的操作不是原子的,但是在64位的JVM上是原子的

实际开发中,商用jvm是解决了这个问题的,我们不用担心

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

这个就是独立操作,这个很好理解,一个原子操作,和另一个原子操作,每个操作,线程执行的时候,一定是不被干扰的,但是这两个原子操作的时候,是不能保证的,只能通过加锁,来保证这一些列操作不会被其他线程干扰

这也能说明,并发中的原子性,就是指的是,线程执行,要执行就执行完,不可再分,不会被其他线程干扰,我认为java中的原子性指的就是这样的。

eg:同步的HashMap也不一定是安全的,先读取,再操作,把每个原子操作组合适用的时候,不是原子性的!

常见问题剖析

到这里就基本上把JMM讲完了,但是总感觉,呀!自己懂了,怎么复述出来,讲功利一点,面试怎么把我脑子里的知识清晰的表达出来!这就是我现在要说的哈!

JMM应用实例

单例模式、单例和并发的关系

单例模式的写法

单例模式

1为什么要用单例模式
节省内存和计算

为什么这样说呢?看这个例子

image-20230818193716017
image-20230818193716017

一个资源,很通用,每次new都会运算和链接很多东西,这就很适合单例模式,加载一次,然后复用

保证结果正确

举个例子,要统计人数,为了加快速度,用多线程去统计,多线程统计,需要一个全局的单例计数器来统计

保证结果正确

方便管理

比如一些工具类,并不需要太多的实例,只需要一个,比如日期工具类,

2适用场景

无状态的工具栏,比如日志工具栏,不需要在实例上存储信息状态,那么一个实例对象即可

全局信息类:比如统计网站访问次数,一个单例模式即可,不论访问哪里,都统统加1,这里就可以用单例模式

饿汉式

代码语言:javascript
复制
/**
 *饿汉式(静态常量) (可用)
 */
public class Singleton1 {
​
    private final static Singleton1 INSTENCE = new Singleton1();
​
    private Singleton1(){
​
    }
    public static Singleton1 getInstance(){
        return INSTENCE;
    }
}

静态代码块

代码语言:javascript
复制
/**
 * 静态代码快  饿汉式 (可用)
 */
public class Singleton2 {
    private final static Singleton2 INSTANCE;
    static {
        INSTANCE = new Singleton2();
    }
    public static Singleton2 getInstance(){
        return INSTANCE;
    }
}

这两类都是饿汉式,优点是简单嘛,缺点浪费资源。没使用,就已经加载好了

懒汉式(线程不安全)【不可用】

代码语言:javascript
复制
public class Singleton3 {
​
    private static Singleton3 instance;
​
    private Singleton3(){
​
    }
​
    public static Singleton3 getInstance(){
        if(instance==null){
            instance = new Singleton3();
        }
        return instance;
    }
​
}
​

并发时,两个线程都判空,会形成两个实例,不符合单例模式要求

懒汉式(线程安全)【不推荐】

代码语言:javascript
复制
public class Singleton3 {
​
    private static Singleton3 instance;
​
    private Singleton3(){
​
    }
​
    public static synchronized Singleton3 getInstance(){
        if(instance==null){
            instance = new Singleton3();
        }
        return instance;
    }
​
}
​

多个线程没法同时创建,不会发生并发问题,但是缺点就是效率太低太低了

比如一个工具类单例,很影响效率的

懒汉式(线程不安全)不推荐

代码语言:javascript
复制
public class Singleton3 {
​
    private volatile static Singleton3 instance;
​
    private Singleton3(){
​
    }
​
    public static synchronized Singleton3 getInstance(){
        if(instance==null){
            synchronized (Singleton3.class){
                instance = new Singleton3();
            }
        }
        return instance;
    }
​
}

这是对前一个的改进,既想满足并发安全,又想提高效率

但是,假设两个线程,都并发的到了synchronized这行,那么就会出现问题!,虽然只能一个线程创建,但是另一个线程等这个线程结束,还是可用创建的!因为他们的都被判断为空。只是先后的差异而已

双重检查【推荐】

代码语言:javascript
复制
public class Singleton4 {
​
    private  static Singleton4 instance;
​
    private Singleton4(){
​
    }
​
    public static Singleton4 getInstance(){
        if(instance==null){
            synchronized (Singleton4.class){
                if(instance == null){
                    instance = new Singleton4();
                }
                
            }
        }
        return instance;
    }
​
}
​

这里就是再判断一次,这样就能解决上面说的,两个线程排队创建实例,第二个线程进来,发生不为null就直接返回了

面试?:为什么要用双重锁?

我:线程安全

单层check行不行?

我:同时进入synchronized,重复了

面试:啊,把synchronized放到方法行不行呀

我:行啊,就是性能太慢了,别人访问的时候,不能即使响应

面试:为什么要用volatile?

我:

这里先在上帝视角分析一下哈,

上面说原子操作,之说变量、引用、以及原子类,我可没有说创建对象是原子操作哈!!

新建对象其实有3个步骤

1、先进行construct empty resource()

创建一个空的对象

2、all constrctor

执行构造方法

3、assign to rs

将对象赋值到引用,也就是代码用的instance

还记得重排序吗,没错,cpu可能会对他们进行重排序的

那么如果 1 3 2这样的顺序,那么双重校验,check,只进行一次的话,就会出现这样很严重的问题

只是有一个引用,但是构造函数还没执行,对象是空的,就会有NPE空指针异常的问题

所以加上volatile!!!!

静态内部类(可用、效率也还行)【归属于懒汉】

代码语言:javascript
复制
public class Singleton7 {
    private Singleton7(){
​
    }
    private static class SingletonInstance{
        private final static Singleton7 INSTANCE = new Singleton7();
    }
    public static Singleton7 getInstance(){
        return SingletonInstance.INSTANCE;
    }
}

外部类加载的时候,是不会把内部类的静态实例加载出来的,所以是懒汉

jvm类加载保证了内部类不会重复的加载这个实例,也就是说保证了线程安全

枚举(推荐)

面试推荐懒汉,因为能体现jmm特性,而枚举适合生产熬!

代码语言:javascript
复制
public enum Singleton8 {
​
    INSTANCE;
    public void whatever(){
        //啥方法都行,这就完成了单例模式!
    }
​
}
class Domo{
    public static void main(String[] args) {
        Singleton8.INSTANCE.whatever();
    }
}

so easy

对比分析

饿汉:

简单,但是没懒加载!

懒汉模式:

需要注意线程安全

静态内部类:

不错,还行,可用

双重检查(属于懒汉)

体现自己对JMM的理解,面试亮点

枚举类

最好,简单,方便,高效,工作适用

面试:那种最好?你知道还挺多

枚举最好

一个大神,在Effective java中表达观点,说是枚举最好

好处:写法简单!, 线程安全有保障, 枚举反编译之后,实质上是一个静态对象,第一次才会加载,

也是懒加载,用到的时候才会加载

还有一个好处!

就是避免反序列化破坏单例、反射破坏

适用枚举的话会避免这两个破坏

适用场合推荐

最好的肯定就是枚举了,既保证线程安全,还有懒加载特性,而且防止反序列化、反射破坏单例

饿汉,就不太好了,一是可能导致开始加载的资源太多,二是有些对象需要依赖配置文件,数据库等,类刚加载的时候是不知道的,就会出现问题,不适用

懒汉式:虽然节省内存,但是对于项目来说,这一点内存不太影响的,但是比如静态内部类,会增加代码复杂性

所以,用枚举最省事了

什么是Java内存模型

上面讲了很多这些概念,我讲了这么多,哎我们怎么说啊??

要从起因说,说来源,比如C语言,不考虑模型规范,不同系统指令不同,

再说java如何做的,通过java内存模型,兼顾操作系统cpu和jvm, 说下jvm内存规范、java对象模型

然后再说java内存模型是一层规范,规范了cpu和jvm在java的匹配,内存模型主要是重排序、可见性、原子性

然后主要讲可见性了,说下jMM对内存的抽象, 讲happens-before,再说volatile和synchronized,讲下原子性和可见性,还有近朱者赤这样的理解,

原子操作?java原子操作有哪些?生成对象是否是原子操作

主要java的原子操作,主要指的是系列操作不会被其他线程执行,

其他的上面讲的很清楚了!

什么是内存可见性

先说cpu多级缓存,再引入到jmm的抽象

long double写入是原子的吗

这里就说的确,32位会分两次,可能有问题的,但是JVM商业版本都以解决,开发不需要注意

volatile和synchronized的区别

这个问题按照上面讲的,说下轻量级,但是也有原子性问题,再结合happens-before,就能答的很完美了!!!

结束!

毕业了毕业了!感谢阅读,别忘了收藏回顾哦!@

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 底层原理
    • 从java代码到cpu指令
      • 三兄弟区分
        • jvm内存结构
          • java对象模型
          • JMM
            • why 需要JMM
              • 规范
                • JMM是工具类、关键字的原理
                  • JMM最最主要内容
                  • 重排序
                    • 代码案例
                      • 重排序的好处:提高处理速度
                        • 重排序的3种情况
                          • 编译器优化:
                          • cpu指令重排
                          • 内存的“重排序”
                      • 可见性
                        • 案例演示
                          • 为什么会有可见性问题?
                            • 救星volatile
                              • JMM的抽象:主内存和本地内存
                                • 什么叫做主内存和本地内存
                                • 主内存与本地内存的关系(很重要)
                              • Happens-Before原则
                                • 什么不是happens-Before
                                • 什么是happends-Before
                                • Happends-Before规则有哪些
                                • 9工具类的Happens-Before
                              • 详解volatile关键字
                                • what is volatile
                                • volatile适用场合
                                • 总结回顾
                              • 保证可见性的措施
                                • 思考:synchronized可见性
                                • 原子性
                                  • 什么是原子性
                                    • java中的原子性
                                      • long和double 的原子性
                                        • 原子操作+原子操作 !=原子操作
                                        • 常见问题剖析
                                          • 单例模式的写法
                                            • 单例模式
                                            • 饿汉式
                                            • 静态代码块
                                            • 懒汉式(线程不安全)【不可用】
                                            • 懒汉式(线程安全)【不推荐】
                                            • 懒汉式(线程不安全)不推荐
                                            • 双重检查【推荐】
                                            • 静态内部类(可用、效率也还行)【归属于懒汉】
                                            • 枚举(推荐)
                                            • 对比分析
                                            • 适用场合推荐
                                          • 什么是Java内存模型
                                            • 原子操作?java原子操作有哪些?生成对象是否是原子操作
                                              • 什么是内存可见性
                                                • long double写入是原子的吗
                                                  • volatile和synchronized的区别
                                              • 结束!
                                              相关产品与服务
                                              容器服务
                                              腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                                              领券
                                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档