java内存模型
1.到底什么叫“底层原理”
1.1 从java到cpu指令
-
最开始,我们编写的Java代码,是.java文件
-
在编译(javac命令)后,从刚才的.java文件会变出一个新的Java字节码文件(*.class)
-
JVM会执行刚才生成的字节码文件(.class),并把字节码文件转化为机器指令
-
机器指令可以直接在CPU上执运行,也就是最终的程序执行
-
JVM实现会带来不同的“翻译” ,不同的CPU平台的机器指令又千差万别,无法保证并发安全的效果一致
-
重点开始向下转移:转化过程的规范、原则
1.2 JVM内存结构 VS Java内存模型 VS Java对象模型
整体方向
-
JVM内存结构,和Java虚拟机的运行时区域有关。
-
Java内存模型,和Java的并发编程有关。
-
Java对象模型,和Java对象在虚拟机中的表现形式有关。
JVM内存结构
堆(Heap):主要放置对象的实列,每次的创建和销毁都会实时的体现在堆中
虚拟机栈(VM stack):保存了各个基本的数据类型,以及对于对象的引用,编译的时候就运行了大小,并且在运行的时候大小不会改变
方法区(Method Area):存储的是已经加载的static变量,或者是类信息以及常量信息,还包含着永久引用(static)
本地方法栈(Native interface):主要放置本地方法
程序计数器:占的区域是最小的,主要是保存当前线程可执行到字节码的行号数,上下文切换的时候会被保存下来,还包括下一条需要执行的指令,分支,等异常处理
java对象模型
java对象
的自身的存储模型
JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类。
当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据。
2.JMM是什么
2.1 为什么需要JMM
C语言不存在内存模型的概念
依赖处理器,不同处理器结果不一样
无法保证并发安全
需要一个标准,让多线程运行的结果可预期
2.2 JMM是规范
全称: Java Memory Model
是一组规范,需要各个JVM的实现来遵守JMM规范,以便于开发者可以利用这些规范,更方便地开发多线程程序。
如果没有这样的一个JMM内存模型来规范,那么很可能经过了不同JVM的不同规则的重排序之后,导致不同的虚拟机上运行的结果不一样,那是很大的问题。
2.3 工具类和关键字的原理
volatile、synchronized、Lock等的原理都是JMM
如果没有JMM
,那就需要我们自己指定什么时候用内存栅栏等,那是相当麻烦的,幸好有了JMM
,让我们只需要用同步工具和关键字就可以开发并发程序。
2.4 最重要三点
重排序
内存可见性
原子性
3. 重排序
3.1 重排序分析
这4行代码的执行顺序决定了最终x和y的结果,一共有3种情况:
- a=1;x=b(0);b=1;y=a(1),最终结果是x=0, y=1
/** 输出结果:x=0 , y=1
* 第一种情况: 线层一进来a=1 ,此时x=0,此时b还没有赋值是0,所以控制台打印x=0
* 线程2 y此时就是1
*
*/
public class Reorder {
private static int a, b, x, y;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
x = b;
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
y = a;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("x=" + x + ",y=" + y);
}
}
- b=1;y=a(0);a=1;x=b(1),最终结果是x=1, y=0
/** 输出结果:x=1 , y=0
* 第二种情况: 叫线程二开始执行
*
*/
public class Reorder2 {
private static int a, b, x, y;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
x = b;
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
y = a;
}
});
t2.start();
t1.start();
t1.join();
t2.join();
System.out.println("x=" + x + ",y=" + y);
}
}
- b=1;a=1;x=b(1);y=a(1),最终结果是x=1, y=1
/**
* 输出结果:x=1 , y=1
* 第三种情况: 使用countDownLatch使两个线程同时执行,随然同时执行了顺序,依然不是百分之白发生
*
*
*/
public class Reorder3 {
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.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
try {
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 == 1 && y == 1) {
System.out.println(result);
break;
} else {
System.out.println(result);
}
}
}
}
会出现x=0,y=0?那是因为重排序发生了4行代码的执行顺序的其中一种可能
y = a;
a = 1;
x = b;
b = 1;
/**
* 输出结果:x=1 , y=1
* 第三种情况: 使用countDownLatch使两个线程同时执行,随然同时执行了顺序,依然不是百分之白发生
*
* x=0 y=0 发生了重排序
*/
public class Reorder3 {
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.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
try {
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.2 重排序的好处
对比重排序前后的指令优化
3.3 重排序的三种情况
编译器优化包括JVM,JIT编译器等
CPU指令重排:就算编译器不发生重排,CPU也可能对指令进行重排
内存的“重排序
” :线程A的修改线程B却看不到,引出可见性问题
4. 可见性
代码演示
/**
* 可见性 四种情况
* a=3, b=3 正常情况
* a =1 b =2 还没有修改就打印
* a= 3 b = 2 a 修改了 b还没有赋值
* a = 1 b =3 a b都修改了但是输出的时候只看到b没看到a ,b线程看不到a线程完整的操作
*/
public class Visibility {
//解决
volatile int a = 1;
volatile int b = 2;
private void update() {
a = 3;
b = a;
}
private void print() {
System.out.println("b=" + b + ",a=" + a);
}
public static void main(String[] args) {
while (true) {
Visibility visibility = new Visibility();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
visibility.update();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
visibility.print();
}
});
t1.start();
t2.start();
}
}
}
4.1 什么是可见性
x原始在主内存是0,更改之后为1,但是线程2并没有直接看到过修改过的,看到的
还是x=0
解决:
使用volatile,会强制刷回主内存
再次读取就是正确的值
4.2 为什么会有可见性问题
CPU有多级缓存导致读的数据过期
高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在CPU和主内存之间就多了Cache层
线程间的对于共享变量的可见性问题不是直接由多核引起的
,而是由多缓存引起的。
如果所有个核心都只用一个缓存,那么也就不存在内存可见性问题了。
每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待
刷入到主存中。所以会导致有些核心读取的值是一个过期的值。
为什么需要JMM
依赖处理器,不同处理器结果不一样
无法保证并发安全
需要一个标准,让多线程运行的结果可预期
4.3 主内存和本次内存
什么是主内存和本地内存
Java 作为高级语言,屏蔽了这些底层细节,用JMM定义了一套读写内存数据的规范
,虽然我们不再需要关心一级缓存和二级缓存的问题
,但是,JMM抽象了主内存和本地内存的概念。
这里说的本地内存并不是真的是一块给每个线程分配的内存,而是JMM 的一个抽象,是对于寄存器、一级缓存、二级缓存等的抽象
。
这张图啊,就把我们主内存和工作内存,给说清楚了,上面的线程呢也是代表核心一个核心,假设运行一个现场的话,他会首先和自己的工作内存,也就是working memory(内存储器)去打交道,这个working memory和working memory之间啊,不同的核心的工作内存之间是不互通的,他们通过一个buff和主内存去沟通,并且呢,他们的交互也只能通过主内存而不能直接的互通,那么我们来说一下,为什么这就照顾到了多方的利益,首先第一个照顾到谁呢?照顾到我们,对于我们而言,我们肯定是不希望有五层内存,那我怎么搞啊?我们作为一个顶层的应用层开发的,你cpu这么复杂,我本身也不想考虑我业务逻辑已经够复杂了。
所以呢,对于我们而言,直接就抽象成两层,我们就不需要考虑太多,我们只知道有两层就够了,比原来的五层已经大大减少,但是我们肯定是希望,你只有一层是最好了,我什么问题都不用考虑,那是最好了,但是这样不行啊,因为我们实际上cpu呢,它确实是存在着自己独占的这个内存,我们如果把它完全抽象成只有主存,就相当于我们把cpu的多层缓存这么一个很好的提高,cpu运行效率的机制给完全浪费了完全荒废了,这样也不行,所以呢,他兼顾到我们java开发者也兼顾了cpu的效率,他把整个抽象成两层,既保证了效率同时呢也大大降低了,开发的难度在这种情况下。
我们再来看另外一张图,也是讲了这个道理左侧的这三个现成的,他们都有自己独自的工作内存,而对工作内存而言,他们要和主内存进行同步,需要一些指令来和主内层达到同步的效果,而主内存同步了之后才能反过来,比如说线程三想找线程一,那么他只能先把自己的内容写到主内存,再从主内存的去通知到内存一,这样也能实现相互之间通信的效果。
JMM有以下规定:
1.所有的变量都存储在主内存中,同时每个线程也有自己独立的工作内存工作内存中的变量内容是主内存中的拷贝
2.线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后再同步到主内存中
3.主内存是多个线程共享的,但线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转来完成
总结:
所有的共享变量存在于主内存中,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题
5. happens-before
5.1什么是happens-before
happens-before规则是用来解决可见性问题的:在时间上,动作A发生在动作B之前,B保证能看见A,这就是happens-before
两个操作可以用happens-before来确定它们的执行顺序:如果一个操作happens-before于另一个操作,那么我们说第一个操作对于第二个操作是可见的。
happens-before演示
加入一个volatile
解决,因为b之前都是可见的。
5.2 happens-before规则有哪些
- 单线程原则
- 锁操作(synchronized和Lock)
- volatile变量
- 线程启动
- 线程join
- 传递性
- 中断
- 构造方法
- 工具类
- 线程安全的容器get一定能看到在此之前的put等存入动作
- CountDownLatch
- Semaphore
- Future
- CyclicBarrier
- 线程池
5.3.1 单线程原则
比如这边段代码,一个线程进来a被修改了,b可以看到a之前所有的操作。
如果发生了重排序,但是也并不影响。
5.3.2 锁操作(synchronized和Lock)
线程B可以看到线程A解锁之前的操作。
线程A拿到锁释放了,线程B拿到锁就可以看到线程A之前操作
5.3.3 volatile变量
只要写入,都可以看到
5.3.4 线程启动
新建线程时, thread. start()一定happens-before run方法里面的语句。
这个是默认的一般不过多考虑。
5.3.5 join
一旦join了,下面的语句可以看到上面的语句运行完毕,在执行。
5.3.6 传递性
如果hb(A, B) 而且hb(B, C),那么可以推出hb(A, C)
5.3.7 中断
一个线程被其他线程interrupt是,那么检测中断( isInterrupted )或者抛出InterruptedException一定能看到。
5.3.8 构造方法
对象构造方法的最后一行指令happens-before于finalize()方法的第一行指令
5.3.9 工具类
- 线程安全的容器get一定能看到在此之前的put等存入动作
- CountDownLatch
- Semaphore
- CyclicBarrier
- Future(默认保证)
- 线程池(默认保证)
6. volatile
6.1 volatile是什么?
volatile是一种同步机制,比synchronized或者Lock相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为。
如果一个变量别修饰成volatile,那么JVM就知道了这个变量可能会被并发修改。
但是开销小,相应的能力也小,虽然说volatile是用来同步的保证线程安全的,但是volatile做不到synchronized那样的原子保护,volatile仅在很有限的场景下才能发挥作用。
6.2 volatile的适用场景
不适用a++场合
输出的结果预期不一致
/*
volatile 不适用的情况
a++ 使用最后值少了好多
*/
public class OrderOperate implements Runnable {
volatile int a = 0;
AtomicInteger atomicInteger = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
a++;
atomicInteger.incrementAndGet();
}
}
public static void main(String[] args) throws InterruptedException {
OrderOperate operate = new OrderOperate();
new Thread(operate).start();
new Thread(operate).start();
Thread.sleep(3000);
System.out.println(operate.a);
System.out.println(operate.atomicInteger);
}
}
输出:18151
20000
适用场合1:
boolean flag,如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。
/*
对于直接赋值 适用于volatile
*/
public class NoVolatile implements Runnable {
volatile boolean done = false;
AtomicInteger atomicInteger = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
done();
atomicInteger.incrementAndGet();
}
}
private void done() {
done = true;
}
public static void main(String[] args) throws InterruptedException {
NoVolatile noVolatile = new NoVolatile();
new Thread(noVolatile).start();
new Thread(noVolatile).start();
Thread.sleep(3000);
System.out.println(noVolatile.done);
System.out.println(noVolatile.atomicInteger);
}
}
就算是boolean 的情况不是直接赋值也不适用于volatile
/*
就算是boolean 的情况不是直接赋值也不适用于volatile
*/
public class NoVolatile1 implements Runnable {
volatile boolean done = false;
AtomicInteger atomicInteger = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
done();
atomicInteger.incrementAndGet();
}
}
private void done() {
done = !done;
}
public static void main(String[] args) throws InterruptedException {
NoVolatile1 noVolatile = new NoVolatile1();
new Thread(noVolatile).start();
new Thread(noVolatile).start();
Thread.sleep(3000);
System.out.println(noVolatile.done);
System.out.println(noVolatile.atomicInteger);
}
}
适用场合2:
作为刷新之前变量的触发器。
public class Visibility {
//解决
int abc =2;
int ab =2;
int a = 1;
volatile int b = 2;
private void update() {
abc = 4;
ab = 7;
a = 3;
b = 0;
}
private void print() {
if (b==0){
//对于b之前的都可以输出正确....
}
}
6.3 volatile两点作用
可见性:读一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个volatile属性会立即刷入到主内存。
禁止指令重排序优化:解决单例双重锁乱序问题
6.4 volatile和synchronized的关系?
volatile在这方面可以看做是轻量版的synchronized:如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。
6.5 volatile解决重排序问题?
之前的重排序加上volatile,永远不会x=0,y=0重排序问题。
6.6 volatile小结
- volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如boolean flag ;或者作为触发器,实现轻量级同步。
- volatile 属性的读写操作都是无锁的,它不能替代 synchronized因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
- volatile只能作用于属性,我们用volatile修饰属性,这样compilers 就不会对这个属性做指令重排序。
- volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile属性不会被线程缓存,始终从主存中读取。
- volatile 提供 happens-before 保证,对 volatile 变量v的写入happens-before所有其他线程后续对v的读操作。
- volatile 可以使得 long 和double的赋值是原子的,后面马上会讲long 和double的原子性。
7. 保证可见性措施
除了volatile可以让变量保证可见性外,synchronized、 Lock、并发集合、Thread.join()和Thread.start()等都可以保证的可见性
具体看happens-before原则的规定
7.1synchronized可见性的正确理解
synchronized
不仅保证了原子性
还保证了可见性synchronized不仅让被保护的代码安全,还近朱者赤
/*
最终执行: 20000
加了synchronized 保证了原子性可见性
*/
public class NoVolatile3 implements Runnable {
private static Object object = new Object();
int a =0;
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
synchronized (object){
a++;
}
}
}
public static void main(String[] args) throws InterruptedException {
NoVolatile3 noVolatile = new NoVolatile3();
new Thread(noVolatile).start();
new Thread(noVolatile).start();
Thread.sleep(3000);
System.out.println(noVolatile.a);
}
}
7.2 Happens-Before规则有哪些?
这里大家可以看到,假设线程a已经释放了锁,b拿到锁,他就能看到现成a里面的所有代码的执行结果,并且synchronized,那一次之前的所有的代码也都会被看到,正如这幅图所述,在解锁之前的所有操作建成b都可以看得到,所以这个实际上也是一个连带效应
,也就代表着我们近朱者赤
,synchronized,不仅让被保护的代码安全了,还让之前的那些代码,也同样可见了。
代码演示:
解锁之前都是可以看到的,上锁之后也可以看到
/**
synchronized 保证了原子性 可见性
*/
public class Visibility1 {
int a = 1;
int b = 2;
int c = 2;
int d = 2;
private void update() {
a = 3;
b = 5;
c = 5;
synchronized (this){
d = 5;
}
}
private void change() {
synchronized (this){
int aa = a;
}
int bb = b;
int cc = c;
int dd = d;
}
.................
8. 原子性
8.1 什么是原子性
一系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分割的。
ATM里取钱 - 原子操作
i++不是原子性的
synchronized实现原子用性
8.2 Java中的原子操作有哪些?
默认的原子操作三种:
除long和double之外的基本类型(int, byte, boolean, short, char,float )的赋值操作
所有引用reference的赋值操作,不管是32位的机器还是64位的机器
java.concurrent.Atomic.*包中所有类的原子操作
8.3 long和double的原子性
官网:Chapter 17. Threads and Locks (oracle.com)
问题描述:官方文档、对于64位的值的写入,可以分为两个32位的操作进行写入、读取错误、使用volatile解决
结论:在32位上的JVM上, long 和double的操作不是原子的,但是在64位的JVM上是原子的
实际开发中:商用Java虚拟机中不会出现
8.4 原子操作+原子操作!=原子操作
简单地把原子操作组合在一起,并不能保证整体依然具有原子性
比如我去ATM机两次取钱是两次独立的原子操作,但是期间有可能银行卡被借给女朋友,也就是被其他线程打断并被修改。
全同步的HashMap也不完全安全
通过索引获取到了一个值,判断如果是想要的值会怎样,不是有是怎样,单列出来是一个个是演奏。原子操作,组合在一起不是。(因为是一个判断操作不是一个确定的值)
9. 单例模式
9.1 单例模式的作用
为什么需要单例:节省内存和计算、保证结果正确、方便管理
节省内存和计算
不经常会被改变,所以我们就希望啊,这些资源我们获取一次全程去通用,在这种情况下呢,我们使用单例模式是很合适的,可以节省我们的内存和计算
保证结果正确
举个例子,有的时候,我们要统计今天来上学的人数,为了加快统计速度,可能是用多线程去统计的,但是多线程去统计的时候,我们依然需要用一个全局的单位计数器来统计,否则如果每个线程都用自己的技术器,那统计的结果就不准了,所以,单例模式,在特定场景下也可以保证结果的正确
方便管理
对于工具类而言,只需要一个实例,这种工具类,在我们实际生产开发中很多啊,比如说日期工具类啊,字符串工具类啊这些就不需要有太多的实例
9.2 单例模式适用场景
无状态的工具类:比如日志工具类,不管是在哪里使用,我们需要的只是它帮我们记录日志信息,除此之外,并不需要在它的实例对象上存储任何状态,这时候我们就只需要一个实例对象即可。
全局信息类:比如我们在一个类上记录网站的访问次数,我们不希望有的访问被记录在对象A上,有的却记录在对象B上,这时候我们就让这个类成为单例。
9.3 单例模式的八种写法
第一种 : 饿汉式(线程不安全)
/*
第一种 : 饿汉式(线程不安全)
*/
public class Singleton1 {
private static final Singleton1 instance = new Singleton1();
private Singleton1() {
}
public static Singleton1 getInstance(){
return instance;
}
}
第二种 : 饿汉式(线程不安全)
静态代码块
/*
第二种 : 饿汉式(线程不安全)
*/
public class Singleton2 {
private static final Singleton2 instance;
static {
instance = new Singleton2();
}
private Singleton2() {
}
public static Singleton2 getInstance(){
return instance;
}
}
第三种 : 懒汉式(线程不安全)
/*
第三种 : 懒汉式(线程不安全)
*/
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;
}
}
第五种 : 懒汉式(没有解决线程安全问题)
/*
第五种 : 懒汉式 (没有解决线程安全问题)
两个线程同时进来,都判断为null,创建了多个实例
*/
public class Singleton5 {
private static Singleton5 instance;
private Singleton5() {
}
public static Singleton5 getInstance() {
if (instance == null) {
synchronized (Singleton5.class) {
instance = new Singleton5();
}
}
return instance;
}
}
第六种:双重检查(检查锁)
/*
第六种:双重检查(检查锁)优点:线程安全。延迟加载,效率较高。
*/
public class Singleton6 {
private static Singleton6 instance;
private Singleton6() {
}
public static Singleton6 getInstance() {
if (instance == null) {
synchronized (Singleton6.class) {
if (instance==null){
instance = new Singleton6();
}
}
}
return instance;
}
}
为什么要用volatile
1.新建对象实际上有3个步骤
2.重排序会带来NPE
3.防止重排序
正确情况下:
先新建实例
调用构造方法
赋值给给左边的引用
重排序:
直接把实列对象赋值给引用
在调用构造方法
但是构造器里面的属性没有计算,没有赋值的,对于里面的rs是空的,但是第二个线程进来的时候看到的rs不是空,因为内部还没准备完毕,第二个线程看到不是空,最后返回结果是空。
第七种:静态内部类方式(推荐用)
/*
第七种:静态内部类 (推荐使用)
达到了线程安全,懒加载的优点
因为只有调用的时候才会加载,并不会创建多个实例
*/
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 test(){
}
}
9.4 不同写法对比
饿汉:简单.但是没有lazy loading
懒汉:有线程安全问题
静态内部类:可用
双重检查:面试用
枚举:最好
9.5 用那种单例实现更好
Joshua Bloch大神在《Effective Java》中明确表达过的观点:“使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举
类型已经成为实现Singleton的最佳方法。
- 写法简答
只需要一行代码
- 线程安全有保障
枚举是一个特殊的类,不需要我们用代码去保证线程安全,经过反编译之后,我们会发现啊枚举实际上会被编译成一个final class,然后继承了枚举这个父类,并且在这个父类中,他的各个实例都是通过static来定义的,所以,枚举的本质,经过反编译之后呢,其实就是静态的一个对象,在这种情况下,我们第一次如果希望使用到我们这个枚举实例的时候,他才会被我们加载进来,所以同样他也是一种懒加载。
- 避免反序列化破坏单例
包括可以使用反射或者反序列化,这样的方法,我们原本保护的那些措施,是可以被这种方法绕过去的。比如说我们用反射就可以把我们私有的各个方法,给绕过去或者反序列化同样可以反序列化多个实例,在这种情况下,我们如果使用枚举就避免了这些安全问题。
9.6 各种写法的适用场合
最好的方法是利用枚举,因为还可以防止反序列化重新创建新的对象
非线程同步的方法不能使用
如果程序一开始要加载的资源太多,那么就应该使用懒加载
饿汉式如果是对象的创建需要配置文件就不适用
懒加载虽然好,但是静态内部类这种方式会引入编程复杂性
10. 常见面试问题?
10.1 讲一讲什么是Java内存模型
为什么当时会有这个?
那么c语言?没有这个会有什么问题?
在多核情况下表现不一致?
简要说明JVM内存结构,java内存模型和java对象模型
再进一步说什么叫做JMM内存模型
它首先是一组规范,规范了,JVM,cpu和java代码之间一系列的转换关系,然后来帮助我们程序员更容易的开
发。
最重要的应该是重排序可见性和原子性这三个部分
然后,就轮到我们开始进一步发挥了,我们可以讲重排序的例子,也可以讲重排序的好处
我们可以把重点放在可见性,这个部分在可见性这里啊,我们要讲一下关于jmm对于我们内存的抽象,抽象成主内存和本地内存这两块,然后我们画个图。
然后,我们再讲很 happens-before原则。
然后讲volatile有关键字,volatile关键字,实际上和可见性也是息息相关的。
我们讲一下volatile适用场合以及他和synchronized的关系
那么既然讲到synchronized关系啊,我们再跟面试官说一下,我们对新宽synchronized的理解是很充分的我们不但通过反编译看过他反编译之后的字节码,还知道synchronized不仅保证原子性,还能保证可见性,并且啊,我们可以利用synchronized近朱者赤
这个特点,不仅能把我们所保护的代码,实现现成安全,还能把附近的代码都做到同步的效果。
最后,我们再讲一下原子性,我们主要想讲,就是关于java中有哪些操作是原子性的,一共就三种。
10.2 volatile和synchronized的异同?
volatile实际上可以看作是轻版的synchronized,volatile指的是他在开销上是比较轻量级的,但是同样既然你开销小,承担的责任呢也会小一些,他适用的场合呢也没有那么广泛,它只适用于始终被各个线程赋值,而没有判断或者累加这种操作。
10.3 什么是原子操作?Java中有哪些原子操作?生成对象的过程是不是原子操作?
10.4 什么是内存可见性?
这张图,这就是体现了为什么会有可见性问题,前面介绍过。