0. 为什么需要单例模式?
- 节省内存和计算
- 保证结果正确
- 方便管理
使用场景:
1. 饿汉式(静态常量)—推荐指数:★★☆☆☆
优点:不会有线程安全问题。
缺点:在类加载的时候就创建对象,如果一直没使用到该对象的话,就造成了内存浪费,如果对象初始化的工作有很多,也会影响到性能。
代码展示:
//饿汉式(静态常量) ---可用
public class Singleton1 {
// 类加载时就创建该实例
private final static Singleton1 INSTANCE = new Singleton1();
private Singleton1(){}
public static Singleton1 getInstance(){
return INSTANCE;
}
}
2. 饿汉式(静态代码块) —推荐指数:★★☆☆☆
优缺点和第一种方式基本一致。
//饿汉式(静态代码块) ---可用
public class Singleton2 {
private final static Singleton2 INSTANCE;
// 以静态代码快的形式创建实例
static {
INSTANCE = new Singleton2();
}
private Singleton2(){}
public static Singleton2 getInstance(){
return INSTANCE;
}
}
3. 懒汉式(线程不安全)—推荐指数:☆☆☆☆☆
优点:使用到的时候才会创建对象,不会造成各种资源浪费问题。
缺点:有线程安全问题。
//懒汉试(线程不安全)---不可用
public class Singleton3 {
private static Singleton3 INSTANCE;
private Singleton3(){}
public static Singleton3 getInstance(){
if (INSTANCE == null){
// 当多个线程同时执行到这里,就会创建多个实例,那各自线程的实例就不是同一个了
INSTANCE = new Singleton3();
}
return INSTANCE;
}
}
4. 懒汉式(线程安全,同步方法)—推荐指数:★☆☆☆☆
优点:使用到的时候才会创建对象,不会造成各种资源浪费问题。
缺点:没有线程安全问题,但是有很大的性能问题,当多个线程同时到达getInstance()方法时,需要排队进入。
这个是在第 3 步的基础上实现的,使用 synchronized 修饰静态方法,由于加上了同步工具类,同一时间只能有一个线程操作,也使得性能下降,所以也不推荐使用:
//懒汉试(线程安全)---不推荐
public class Singleton4 {
private static Singleton4 INSTANCE;
private Singleton4(){}
public synchronized static Singleton4 getInstance(){
if (INSTANCE == null){
INSTANCE = new Singleton4();
}
return INSTANCE;
}
}
5. 懒汉式(线程不安全,同步代码块)—推荐指数:☆☆☆☆☆
优点:使用到的时候才会创建对象,不会造成各种资源浪费问题。
缺点:线程不安全,还有性能问题!多个线程在synchronized那一行排队,进入代码块后一样会创建多个对象。
这个是在第 4 步的基础上作进一步尝试,虽然性能上的问题解决了,但是又出现了线程不安全的问题,如下:
//懒汉试(线程不安全,同步代码块)---不可用
public class Singleton5 {
private static Singleton5 INSTANCE;
private Singleton5(){}
public static Singleton5 getInstance(){
if (INSTANCE == null){
// 如果多个线程同时执行到这里,依然会创建多个实例
synchronized (Singleton5.class){
INSTANCE = new Singleton5();
}
}
return INSTANCE;
}
}
6. 懒汉式(双重检查)—推荐指数:★★★★☆
优点:使用到的时候才会创建对象,不会造成各种资源浪费问题。
缺点:复杂。
//双重检查---推荐使用
public class Singleton6 {
private static Singleton6 INSTANCE;
private Singleton6(){}
public static Singleton6 getInstance(){
if (INSTANCE == null){
synchronized (Singleton6.class){
//再做一次检查:由于同时只能有一个线程进入到这里,所以此时的 INSTANCE 如果还是 null,那么就再创建,创建之后有且仅有这一个实例
if (INSTANCE == null){
INSTANCE = new Singleton6();
}
}
}
return INSTANCE;
}
}
这个优化我们利用了双重检测机制和同步锁,这种方式也称为双重同步锁单例模式
,但是这个案例还是线程不安全的,大家通过代码层面的分析后,发现确实不会有线程安全问题,那问题出现在哪呢?这个其实要和对象创建步骤和JVM 指令重排挂钩,我们正常创建对象的指令步骤是这样的:
- memory = allocate() 分配对象的内存空间
- ctorInstance() 初始化对象,执行对应的构造方法
- instance = memory 设置instance指向刚分配的内存
但是因为JVM和cpu优化,发生了指令重排,执行顺序如下:
- memory = allocate() 分配对象的内存空间
- instance = memory 设置instance指向刚分配的内存
- ctorInstance() 初始化对象
我们可以结合代码,假如A线程进入同步代码块执行 instance = new Singleton6(),执行到“instance = memory 设置instance指向刚分配的内存”,这个时候B线程在第一次执行“if (instance == null)”,发现instance不为空,直接返回instance实例,其实线程B得到的这个实例并没有完全初始化(A还没有执行完对象的初始化步骤)就已经使用了。
那如何禁止指令重排呢,很简单,用我们前面文章提到的volatile关键字就可以了
在 INSTANCE 前加上 volatile 关键字来修饰,代码如下:
//双重检查---推荐使用
public class Singleton6 {
private volatile static Singleton6 INSTANCE;
private Singleton6(){}
public static Singleton6 getInstance(){
if (INSTANCE == null){
synchronized (Singleton6.class){
//再做一次检查:由于同时只能有一个线程进入到这里,所以此时的 INSTANCE 如果还是 null,那么就再创建,创建之后有且仅有这一个实例
if (INSTANCE == null){
INSTANCE = new Singleton6();
}
}
}
return INSTANCE;
}
}
7. 静态内部类 — 推荐指数:★★★☆☆
优点:使用到的时候才会创建对象,不会造成各种资源浪费问题,线程安全。
缺点:没有太大缺点。
由于类在初始化时,并不会初始化静态内部类中的实例,所以这属于饿汉式单例:
//静态内部类 --- 推荐使用
public class Singleton7 {
private Singleton7(){}
private static class SingletonInstance{
//JVM会保证构造方法的线程安全问题,即使多个线程同时访问 getInstance() 方法,也只会创建一个实例,这是 JVM 保证的
private static final Singleton7 INSTANCE = new Singleton7();
}
public static Singleton7 getInstance(){
return SingletonInstance.INSTANCE;
}
}
8. 枚举 — 推荐指数:★★★★★
优点:使用到的时候才会创建对象,不会造成各种资源浪费问题,线程安全。
缺点:最优方案。
//枚举 --- 生产实践推荐使用
public enum Singleton8 {
INSTANCE;
//方法
public void whatever(){ }
}
使用的时候只需要调用Singleton8.INSTANCE
,比如这里想调用该类中的 whatever()
方法,只需要执行 Singleton8.INSTANCE.whatever()