单例模式
1、定义
单例模式是一种创建型的设计模式。
采用一定的策略保证整个软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。
单例模式常常被用来管理共享的资源,如数据库连接或者连接池。
单例模式的实现有多种方式,接下来将详解常见的几种
单例模式实现
1、饿汉式(静态常量)
实现步骤
- 构造器私有化(防止外界new)
- 在类的内部创建对象
- 对外暴露一个静态方法getInstance,获取对象实例
代码实现
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton(){}
//外界通过该方法获取实例
public static Singleton getInstance(){
return INSTANCE;
}
}
从代码中,我们INSTANCE
被static修饰,所以在程序启动的时候就会被创建,即使没有被使用到。这就是饿汉式名称的由来。
接下来就是如何获取对象实例了
public class Test {
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
//打印true
System.out.println(instance1 == instance2);
}
}
饿汉式优缺点
优点:代码简单,类的实例化过程是在加载的时候完成,所以不存在线程安全问题。
缺点:如果实例没有被使用到,就可能造成内存浪费。
2、懒汉式
定义
为了避免饿汉式可能造成的内存浪费,懒汉式孕育而生。所谓的懒汉式就是单例对象只有在被使用到的时候采用创建。
懒汉式的实现方式也有多种,下面介绍俩种线程安全的写法:
使用synchronized实现线程安全
class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
将synchronized
加在方法级别上虽然可以解决线程安全,但是存在效率问题。因为当synchronized
加在方法级别上时,意味着每次进入该方法的时候都需要进行加锁。但是实际上,我们只有第一次调用的时候才需要进行同步加锁,所以会存在性能损耗。
使用双重检查可以解决这个问题。
使用双重检查实现线程安全
public class Singleton {
private static volatile Singleton INSTANCE;
private Singleton(){}
public static Singleton getInstance(){
if (INSTANCE == null){
synchronized (Singleton.class){
if (INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
在双重检查机制中,第一次调用时,INSTANCE == null
条件成立,会进行同步,后面的的调用将不会进入if代码块中,也就不会有同步。
这里之所以要使用俩个if是因为在多线程环境下,可能同时多个线程进入该方法,同时通过了第一个if判断。但当第一个线程创建后,后面的线程就不该创建了,所以需要第二个if来进行判断。
3、静态内部类实现
代码实现
class Singleton {
private static volatile Singleton instance;
//构造器私有化
private Singleton() {}
//写一个静态内部类,该类中有一个静态属性 Singleton
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
//提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE
public static synchronized Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
public class Test {
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1 == instance2);
}
}
分析
这里利用了静态内部类的加载机制:
- 外部类初次加载时,会初始化静态变量、静态代码块、静态内部类,但不会加载内部类和静态内部类
- 调用外部类的静态方法、静态变量时,外部类必须先进行加载,但只加载一次
- 直接调用静态内部类时,外部类不会加载
综上,由于静态内部类只有在使用到的时候,才会被加载,所以instance
是在使用到的时候才会被创建。此外由于是JVM加载的时候创建,所以也是线程安全的。
结论,静态内部类实现单例拥有以下特点:
- 属于懒加载
- 线程安全
静态内部类可能存在的问题
不止静态内部类,之前说的单例实现在一些特殊情况下都可能出现问题:
- 使用反射强行创建对象
- 使用序列化和反序列化导致创建多个对象实例
使用反射破坏单例模式:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//使用静态内部类获取单例对象
Singleton instance = Singleton.getInstance();
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
// 通过反射强行创建对象
Singleton singleton = declaredConstructor.newInstance();
System.out.println(singleton == instance);//打印false
}
使用序列化反序列化破坏单例:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException {
//使用静态内部类获取单例对象
Singleton instance = Singleton.getInstance();
//序列化
ByteArrayOutputStream aos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(aos);
oos.writeObject(instance);
//反序列化
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(aos.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Singleton singleton = (Singleton) objectInputStream.readObject();
//打印false
System.out.println(instance == singleton);
//关闭资源...
}
https://blog.csdn.net/qq_28181131/article/details/82258877
4、枚举实现单例模式
enum Singleton {
INSTANCE; //属性
public void sayOK() {
System.out.println("ok~");
}
}
使用枚举的优势:
- 借助DK1.5 中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
- 枚举可以防止反射强行创建多个实例
为什么单例可以防止序列化重新创建新的对象
为什么单例可以防止反射强行创建多个实例
枚举可以在研究研究。
问题环节
1、能不能创建一个类,并把所有的方法和变量都定义为静态的,把类直接当作一个单例?
如果你的类完全自给自足,而且不依赖复杂的初始化,那么你可以这么做。但是,因为静态初始化的控制权在Java手上,这么做有可能导致混乱,特别是当许多类涉及其中的时候,常常会造成一些微妙的、不容易发现的与初始化次序相关的BUG。除非你有绝对的必要使用类的单例,否则还是建议使用对象的单例,这样比较保险。