定义
一个类只能有一个实例并且提供一个全局访问点。
实现方式
创建时实例化
实例化一个静态对象,在虚拟机加载类环节会初始化对象。优点是实现简单,缺点是该单例对象一开始就已经创建,如果该对象并不常用,会比较浪费内存资源。
public class Singleton1 {
private static Singleton1 singleton1 = new Singleton1();
private Singleton1() {
}
public static Singleton1 getInstance() {
return singleton1;
}
}
延迟初始化
这种方式做到了延迟初始化,但是在多线程并发访问getInstance()方法时,会出现实例化多个对象的情况。
public class Singleton2 {
private static Singleton2 singleton2;
private Singleton2() {
}
public static Singleton2 getInstance() {
if (singleton2 == null) {
singleton2 = new Singleton2();
}
return singleton2;
}
}
延迟初始化多线程版本
public class Singleton3 {
private static Singleton3 singleton3;
private Singleton3() {
}
public synchronized Singleton3 getInstance() {
if (singleton3 == null) {
singleton3 = new Singleton3();
}
return singleton3;
}
}
延迟初始化
此处采用了双端检测的方法,在1)和3)处进行了两次的check。此处有一个非常容易让人忽略的点,也是面试被高频问到的一个知识点:singleton3为什么要添加volatile修饰符?
原因的分析如下:
singleton3对象的创建并非原子操作,可以简单的分解成下面三个步骤:
- 分配内存空间;
- 初始化对象;
- 指向对象;
第2步和第3步由于CPU指令重排序,可能执行次序会发生变化。假如线程A获取实例对象没有获取到,加锁初始化对象,执行完第1步后执行第3步;此时线程B获取实例对象,因为对象已经指向了分配的内存空间,所以此时能够获取到对象,但是该对象该没有初始化完毕,对象的属性值还可能是“0”值,在使用时会产生问题。而volatile修饰符有一个重要的作用就是禁止指令重排序。这样就不会发生对象中数据不符合预期的情况。
public class Singleton4 {
private static volatile Singleton4 singleton3;
private Singleton4() {
}
public static Singleton4 getInstance() {
if (singleton3 == null) {//1.如果不为空,返回已经初始化完毕的对象
synchronized (Singleton4.class) {//2.加锁
if(singleton3 == null){//3.double check,判断对象是否已经初始化完毕
singleton3 = new Singleton4();//4.如果没有初始化完毕,则初始化
}
}
}
return singleton3;
}
}