单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。如果我们需要让某一个类在整个程序生命周期内只能有一个实例,那么就要使用单例模式。
想要实现单例模式,必须满足三个必要条件:
单例设计模式一般分为如下两类:
实现的基本思路如下:
getInstance
方法;静态变量法是最基本的一个实现,通过静态变量的方式创建唯一对象,其实现如下:
public class Singleton1 {
// 类的内部创建对象;
private static Singleton1 instance = new Singleton1();
// 构造器私有化
private Singleton1() {
}
// 向外暴露静态方法
public static Singleton1 getInstance() {
return instance;
}
}
除此之外还有一种使用静态代码块的构建方法
public class Singleton2 {
private static Singleton2 instance;
// 使用静态代码块创建静态示例;
static {
instance = new Singleton2();
}
private Singleton2() {
}
public static Singleton2 getInstance() {
return instance;
}
}
为了解决饿汉式类加载造成的内存浪费,可以采用懒加载的方式创建实例,以下是懒汉式的最基本实现。
public class Singleton3 {
private static Singleton3 instance;
private Singleton3() {
}
// 可能出现线程安全问题
public static Singleton3 getInstance() {
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}
这种方式是最基本的实现方式,其最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。这种方式不要求线程安全,在多线程不能正常工作。
由于懒汉式基础实现有多线程安全问题,因此最简单的解决方案就是加锁,其实现如下:
public class Singleton4 {
private static Singleton4 instance;
private Singleton4() {
}
public static synchronized Singleton4 getInstance() {
if (instance == null){
instance = new Singleton4();
}
return instance;
}
}
这种方式能够在多线程中很好的工作,同时能够实现懒加载。但是锁的粒度很大,同步效率低。
public class Singleton5 {
private static Singleton5 instance;
private Singleton5() {
}
// 双重检查
public static Singleton5 getInstance() {
if (instance == null) {
synchronized (Singleton5.class) {
if (instance == null) {
instance = new Singleton5();
}
}
}
return instance;
}
}
双重检查实现方式既能保证线程安全,也能实现懒加载,同时其效率较高,在多线程情况下能保持高性能。
使用静态内部类的优点在于:
使用静态内部类可以天然的实现线程安全的懒加载单例类,其具体实现如下:
public class Singleton6 {
private static Singleton6 instance;
private Singleton6() {
}
public static Singleton6 getInstance() {
return InnerSingleton6.instance;
}
// 使用静态内部类实现单例
public static final class InnerSingleton6 {
private static Singleton6 instance = new Singleton6();
}
}
类加载器加载 Singleton6时不定会加载 InnerSingleton6 只有显式的调用 getInstance
方法时才会加载内部类,从而实例化 instance。这种实现由 JVM 保证类加载时的线程安全,效率非常高。
这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,防止多次实例化。这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。其唯一的问题在于枚举类永远继承自 Enum,且无法被继承;
public enum Singleton7 {
INSTANCE;
public void otherMethod() {
// ....
return;
}
}
上面的这种实现是一种饿汉式实现,也可以将业务类放在一个枚举类中:
public enum Singleton8 {
INSTANCE;
private User user;
Singleton8() {
user = new User();
}
public User getUser() {
return user;
}
public static class User {
}
}
可以使用序列化和反序列破坏单例模式,以下面单例类为例:
public class Singleton1 implements Serializable {
// 类的内部创建对象;
private static Singleton1 instance = new Singleton1();
// 构造器私有化
private Singleton1() {
}
// 向外暴露静态方法
public static Singleton1 getInstance() {
return instance;
}
}
测试类:
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Singleton1 oriInstance = Singleton1.getInstance();
// 创建对象输出流并输出对象
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("obj.txt")));
oos.writeObject(oriInstance);
// 释放资源
oos.close();
// 创建对象输入流并读取
ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get("obj.txt")));
Singleton1 readInstance = (Singleton1) ois.readObject();
// 释放资源
ois.close();
// false
System.out.println(oriInstance == readInstance);
}
}
解决方案:在单例类中定义一个 readResolve
方法,该方法在反序列化时被调用;以 ObjectInputStream 源码为例:
class ObjectInputStream{
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
passHandle = handles.assign(unshared ? unsharedMarker : obj);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(passHandle, resolveEx);
}
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}
handles.finish(passHandle);
// 如果有readResolve方法,则调用该方法
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
}
通过反射同样可以破坏单例模式,其测试类如下:
public class test {
public static void main(String[] args) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
// 获取字节码文件
Class<Singleton1> singleton1Class = Singleton1.class;
// 获取获取无参构造器
Constructor<Singleton1> constructor = singleton1Class.getDeclaredConstructor();
// 设置访问权限
constructor.setAccessible(true);
Singleton1 singleton1 = constructor.newInstance();
Singleton1 singleton2 = constructor.newInstance();
// false
System.out.println(singleton1 == singleton2);
}
}
解决方案:在私有构造器中加上一个判断,如果对象二次创建,则直接抛出异常。结合防止序列化和反序列化的方式,单例类的代码修改如下:
public class Singleton1 implements Serializable {
// 类的内部创建对象;
private static Singleton1 instance = new Singleton1();
// 放置反射破坏单例
private static boolean uniqueInstance = false;
// 构造器私有化
private Singleton1() {
if (uniqueInstance) {
throw new RuntimeException("重复构建对象");
}
uniqueInstance = true;
}
// 向外暴露静态方法
public static Singleton1 getInstance() {
return instance;
}
// 放置序列化反序列化破坏单例
public Object readResolve() {
return instance;
}
}
一般情况下,使用静态变量法实现饿汉式;如果有实现懒加载的需求时使用静态内部类的方式进行实现;如果涉及到反序列化创建对象时,可以尝试使用枚举方式。最后才考虑使用双重检查的方式实现单例。
文中难免会出现一些描述不当之处(尽管我已反复检查多次),欢迎在留言区指正,相关的知识点也可进行分享,希望大家都能有所收获!!如果觉得我的文章写得还行,不妨支持一下。你的每一个转发、关注、点赞、评论都是对我最大的支持!