1. 单例模式
1.1 介绍
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。
这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式的特点
- 构造方法私有、实例化的变量引用私有、获取实例的方法共有
1.2 优缺点
优点
- 在内存中只有一个实例,减少了内存的开销,尤其是频繁地创建和销毁实例
- 避免对资源的多重占用
缺点
- 没有接口,不能继承,与单一职责原则冲突,一个类不应该关心外面怎么样来实例化
- 容易出现线程安全问题
1.3 使用场景
- 主要为了避免一个全局使用的类频繁创建和销毁
1.4 注意事项
2. 代码实现
饿汉模式 :初始化时创建,线程安全,但浪费资源
public class Singleton
{
private static Singleton instance = new Singleton(); // 类加载时初始化,浪费资源
private Singleton(){};
public static Singleton getInstance()
{
return instance;
}
}
懒汉模式 :用时创建,线程不安全
public class Singleton
{
private static Singleton instance;
private Singleton(){};
// 对外提供该类实例
public static Singleton getInstance()
{
if(instance == NULL)
{
instance = new Singleton();
}
return instance;
}
}
懒汉模式:线程安全,但开销大
public class Singleton
{
private static Singleton instance;
private Singleton(){};
// 对外提供该类实例
public static synchronized Singleton getInstance()
{
if(instance == NULL)
{
instance = new Singleton();
}
return instance;
}
}
双重校验锁:线程安全,需要禁止指令重排,不防反射/序列化
public class Singleton
{
// 指令重排是指JVM在实例化对象时会将如下三步重新排序:
// 1. 分配内存空间 2. 初始化对象 3. 将初始化对象指向分配的内存空间
private volatile static Singleton singleton; // 给变量加锁,禁止指令重排
private Singleton(){};
public static Singleton getInstance()
{
if(singleton == NULL)
{
synchronized(Singleton.class) // 这个地方的锁被当前线程占领直至创建对象成功
{
if (singleton == null) // 为什么要两次判空
{
singleton = new Singleton();
}
}
}
return instance;
}
}
为什么要两次判空:
首先介绍两者的功能,第一个if是为了减少性能开销,第二个if是为了避免生成多个实例。
-
如果只有外面的if,则会生成多个对象,
-
如果只有里面的if,则每次都会加锁并判空。
枚举类型:初始化时创建,线程安全,防反射/序列化
public enum Singleton{
//定义1个枚举的元素,即为单例类的1个实例
INSTANCE;
// 隐藏了1个空的、私有的 构造方法
// private Singleton () {}
}
// 获取单例的方式:
Singleton singleton = Singleton.INSTANCE;
登记式/静态内部类:使用静态内部类,即避免了类加载时实例化对象,同时可以实现单例模式。
public class Singleton
{
private Singleton (){};
// 静态内部类会在第一次使用时才被加载和初始化
private static class SingletonHolder
{
private static final Singleton INSTANCE = new Singleton();
}
public static final Singleton getInstance()
{
return SingletonHolder.INSTANCE;
}
}
静态内部类和双重检验锁的比较:
在早期的JVM中,同步(甚至是无竞争的同步)都存在着巨大的性能开销。因此,人们想出来了许多“聪明的”技巧来降低同步的影响,有些技巧很好,有些技巧是不好的,甚至是糟糕的,DCL就属于“糟糕”的一类。
DCL的真正问题在于:当在没有同步的情况下读取一个共享对象时,可能发生的最糟糕的事情只是看到一个失效值(在这种情况下是一个空值),此时DCL方法将通过在持有锁的情况下再次尝试来避免这种风险。然而实际情况远比这种情况糟糕——线程可能看到引用的当前值,但对象的状态值却是失效的,这意味着线程可以看到对象处于无效或错误的状态。也就是说,JVM早期的对象创建很慢,因此可能在持有锁时读到空对象。
然而,DCL的这种使用方法已经被广泛地废弃了——促使该模式出现的动力(无竞争同步的执行速度很慢,以及JVM启动时很慢)已经不复存在了,因为它不是一种高效地优化措施。延迟初始化占位类模式(静态内部类)能带来同样的优势,并且更容易理解。
通过反射破坏单例模式
public static void main(String[] args) {
Class<Girlfriend_2> clazz = Girlfriend_2.class;
try {
// 通过反射获取私有构造方法
Constructor<Girlfriend_2> declaredConstructor = clazz.getDeclaredConstructor(null);
// 强制访问
declaredConstructor.setAccessible(true);
// 通过修改后的构造方法创建实例
Girlfriend_2 girlfriend1 = declaredConstructor.newInstance();
Girlfriend_2 girlfriend2 = declaredConstructor.newInstance();
System.out.println(girlfriend1);
System.out.println(girlfriend2);
} catch (Exception e) {
e.printStackTrace();
}
}
通过序列化/反序列化破坏单例模式
public static void main(String[] args) throws Exception {
Singleton_2 instance = Singleton_2.getInstance3();
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("temp"));
oos.writeObject(instance);
oos.flush();
oos.close();
// 反序列化
FileInputStream fis = new FileInputStream("temp");
ObjectInputStream ois = new ObjectInputStream(fis);
Singleton_2 object = (Singleton_2) ois.readObject();
ois.close();
System.out.println(instance); // 两者一定不同
System.out.println(object);
}
因为readObject内部通过反射新建了一个实例,并返回。可以通过在单例类中重写readResolve方法防止通过序列化破坏单例模式
private Object readResolve(){
return instance;
}
通过枚举类型避免反射/序列化破坏单例模式
反序列化中通过反射构造对象,而枚举类天然地避免了反射攻击。
3. 源码实现
3.1 java.lang.Runtime
public class Runtime {
private static Runtime currentRuntime = new Runtime(); // 饿汉式
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
}