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并发核心知识体系精讲》