单例模式
1 实现种类
0 饿汉式
0.1 静态常量方式
/**
* 静态常量方式
* @author gu
* @date 2020/6/6 21:54
*/
public class Singleton1 {
// 私有化构造方法
private Singleton1(){}
// 静态变量
private final static Singleton1 instance = new Singleton1();
// 对外接口,返回实例
public static Singleton1 getInstance(){
return instance;
}
}
0.2 静态代码块方式
/**
* 静态代码块方式
* @author gu
* @date 2020/6/6 21:54
*/
public class Singleton1 {
// 私有化构造方法
private Singleton1(){}
// 静态变量
private static Singleton1 instance;
// 静态代码块
static {
// todo 可做其他初始化时的操作
instance = new Singleton1();
}
// 对外接口,返回实例
public static Singleton1 getInstance(){
return instance;
}
}
0.3 饿汉式实现分析
- 两种实现方式都是在类加载的时候实例化的;避免了线程同步问题,天然扛并发。
- 在类加载时实例化,没有达到
lazy loading
的效果;会造成内存浪费。 - 两种恶汉方式相比,静态代码块方式可以在静态代码开中添加额外的初始化代码。
1 懒汉式
1.1 非线程安全
/**
* 非线程安全 懒汉式
* @author gu
* @date 2020/6/6 21:54
*/
public class Singleton1 {
// 静态变量
private static Singleton1 instance;
// 私有化构造方法
private Singleton1(){}
/**
* 提供一个静态对外方法,当使用时再创建
*/
public static Singleton1 getInstance(){
if(instance == null){
instance = new Singleton1();
}
return instance;
}
}
- 起到了Lazy Loading的效果,但只能单线程下使用。
- 如果在多线程下,一个线程进入了
if(singleton == null)
语句,还没来得及执行下面的语句,另一个线程也通过了这个判断语句,这时便会产生多个实例。 - 实际开发中不要使用该方式。
1.2 线程安全,同步方法方式
/**
* 同步方法方式
* @author gu
* @date 2020/6/6 21:54
*/
public class Singleton1 {
// 静态变量
private static Singleton1 instance;
// 私有化构造方法
private Singleton1(){}
/**
* 同步改实例化方法
* 提供一个静态对外方法,当使用时再创建
*/
public static synchronized Singleton1 getInstance(){
if(instance == null){
instance = new Singleton1();
}
return instance;
}
}
- 解决了线程安全问题。
- 效率低,每次调用
getInstance()
方法时都要进行同步,而这个方法只需要第一次调用的时候同步就行了,后续的调用直接return就行了。 - 实际开发不推荐。
1.3 线程安全,同步代码块
/**
* 同步代码块方式
* @author gu
* @date 2020/6/6 21:54
*/
public class Singleton1 {
// 静态变量
private static Singleton1 instance;
// 私有化构造方法
private Singleton1(){}
/**
* 同步代码块
* 提供一个静态对外方法,当使用时再创建
*/
public static Singleton1 getInstance(){
if(instance == null){
synchronized(Singleton1.class){
instance = new Singleton1();
}
}
return instance;
}
}
- 可以避免多次进行同步的问题,但是带来了和1.1中相同的线程安全问题。两个线程会同时进入
if判断里面
。 - 不可以使用。
1.4 双重检查
/**
* 双重检查
* @author gu
* @date 2020/6/6 21:54
*/
public class Singleton1 {
// *********volatile修饰变量,避免instance = new Singleton1();时指令重排**********
private static volatile Singleton1 instance;
// 私有化构造方法
private Singleton1(){}
/**
* 提供一个静态对外方法,当使用时再创建
*/
public static Singleton1 getInstance(){
if(instance == null){
synchronized (Singleton1.class){
// ***********确保当多个线程进入第一个if判断时,只有一个线程能进入第二个if做实例化操作*******
if(instance == null){
instance = new Singleton1();
}
}
}
return instance;
}
}
volatile修饰变量,避免instance = new Singleton1();时指令重排*
****确保当多个线程进入第一个if判断时,只有一个线程能进入第二个if做实例化操作
- double-check概念是多线程开发中常使用的,通过两次的if检查,保证线程的安全。
- 线程安全,效率高,延迟加载。
- 推荐使用。
2 静态内部类方式
/**
* 静态内部类
* @author gu
* @date 2020/6/6 21:54
*/
public class Singleton1 {
// 私有化构造方法
private Singleton1(){}
// 静态内部类,用静态属性实例化
private static class SingletonIinstance{
private static final Singleton1 INSTANCE = new Singleton1();
}
// 对外方法,直接返回静态内部类的INSTANCE
public static synchronized Singleton1 getInstance(){
return SingletonIinstance.INSTANCE;
}
}
- 外部类加载时并不需要立即加载内部类,内部类不被加载。
- 由于没有被加载,则不去初始化INSTANCE,故而不占内存。即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE。
- 第一次调用getInstance()方法会导致虚拟机加载SingletonIinstance类,被加载时INSTANCE只会被初始化一次。这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
- 推荐使用。
3 上述模式的缺点
3.1 序列化可能破坏单例
解决方案是单列类实现Serializable
接口。
3.2 使用强反射调用私有构造器破坏单例
解决方案:修改构造器,当创建第二个对象时抛出异常
private static volatile boolean flag = true;
// 私有化构造方法
private Singleton1(){
if(flag){
flag = false;
}else{
throw new RuntimeException("The instance already exists!");
}
}
4 枚举方式
public enum SingletonEnum {
INSTANCE;
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
// 可以自定义方法
public void sayOK() {
System.out.println("ok~");
}
}
- 使用枚举类
SingletonEnum.INSTANCE
进行访问。 - 枚举序列化是由jvm保证的,每一个枚举类型和定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定:在序列化时Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的并禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,从而保证了枚举实例的唯一性。
有关枚举的相关知识参考zejian的博客的深入理解Java枚举类型(enum)一文。
文中指出枚举方式的单例缺点:枚举相比静态常量占用内存过大,不适用于Android开发。
2 单例模式被应用源码分析
JDK中的应用
在jdk中java.lang.Runtime
就是单例模式
单例模式的使用细节说明和注意事项
- 单例保证了系统只有一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。
- 当想实例化一个单例对象的时候,必须记住使用相应的对象方法,而不是使用new。
- 单例模式使用的场景:需要频繁的进行创建和销毁的对象,创建对象时耗时过多或者耗费的资源过多(重量级对象),但是又要经常用到的对象如:工具类对象,频繁访问数据库或文件的对象。