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 long
和double
的原子性
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
静态内部类[推荐用]
/**
* 静态内部类 可用(属懒汉)
* 优点:线程安全;懒加载
*/
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的最佳方法。”
写法简单
线程安全有保障:枚举是一种特殊的类,不需要我们保证代码安全,经过反编译,枚举会被
编译成final
class,继承了枚举这个父类,并且在这个父类中它的各个实例都是通过static来定义的。枚举的本质是静态的对象。
避免反序列化破坏单例:防止反序列化创建新的对象。
非线程安全的方式不能使用
如果程序一开始要加载的资源太多,那么就应该使用
懒加载
饿汉式如果是对象的创建需要配置文件就不适用。
懒加载虽然好,但是静态内部类这种方式会引入编程复杂性。