设计模式
单例模式
单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。
其特点是:
- 单例类只能有一个实例;
- 单例类必须由自己来创建自己的实例;
- 单例类必须向外部提供实例访问接口。
单例模式可以保证内存里只有一个实例,减少了内存的开销,还可以避免对资源的多重占用。
实现单例模式的方式主要有三种:饿汉式单例模式、懒汉式单例模式、注册式单例模式。
一、饿汉式单例模式
饿汉式单例模式在类加载的时候就会立即初始化并创建唯一实例对象,它是线程安全的,因为在线程还没出现以前就实例化了,不存在访问安全的问题。
**优点:**本身就是线程安全的,不需要通过其他机制来实现线程安全,所以效率会比较高;
**缺点:**不管是否会用到该实例,它都会在类加载的时候初始化并占据一定的内存空间,可能会造成内存浪费(占着茅坑不拉屎)。
饿汉式单例模式的实现:
// 方式一
public class HungrySingleton {
// 两种方式实现饿汉式单例模式
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
// 方式二
public class HungrySingleton {
private static final HungrySingleton hungrySingleton;
static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){}
public static HungrySingleton getHungrySingleton() {
return hungrySingleton;
}
}
**注意:**单例类的构造方法要限定为private的,并且需要为外部提供一个getInstance()接口来访问实例,避免单例类在外部被实例化。
二、懒汉式单例模式
与饿汉式单例模式不同,懒汉式单例模式是指该实例被外部需要的时候才会进行初始化。
**优点:**由于是在外部需要的时候才会进行初始化,所以不会出现饿汉式单例模式那种“占着茅坑不拉屎”的情况;
**缺点:**不是线程安全的,在多线程的情况下可能会出现初始化出多个实例;
懒汉式单例模式的实现:
// 简单实现,没有提供线程安全
public class LazySingleton {
private LazySingleton(){}
private static LazySingleton lazy = null;
public static LazySingleton getInstance() {
if (lazy == null) {
lazy = new LazySingleton();
}
return lazy;
}
}
采用以下方式来检测线程不安全的情况:
public class ExectorThread implements Runnable{
@Override
public void run() {
LazySingleton singleton = LazySingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ":" + singleton);
}
}
public class LazySimpleSingletonTest {
public static void main(String[] args) {
new Thread(new ExectorThread()).start();
new Thread(new ExectorThread()).start();
/*
该程序运行的结果会有两种情况:两个线程输出的单例实例是同一个,或者两个线程输出的单例实例不是同一个(多运行几次就会出现两种情况)
如果输出不是同一个就很明显创建了两个单例实例;
如果输出是同一个也不能说明只创建一个单例实例,有可能是一个线程创建实例赋值给lazy,另一个线程同时也创建了一个实例赋值给了lazy,
后者覆盖了前者创建的实例,所以导致getInstance()返回的是同一个实例,但实际上可能创建了两个
*/
}
}
可以采用加锁的方式来实现线程安全:
public class LazySingleton {
private LazySingleton(){}
private static LazySingleton lazy = null;
// 使用synchronized来解决线程安全问题
public synchronized static LazySingleton getInstance() {
if (lazy == null) {
lazy = new LazySingleton();
}
return lazy;
}
}
或者这样:
public class LazySingleton {
private LazySingleton(){}
private volatile static LazySingleton lazy = null;
/*
该方式会比上面的好,原因是上面在多线程访问的时候会阻塞在方法哪里,不管实例是否已经创建都会阻塞
而这种方法阻塞的位置是创建实例的时候,如果已经创建好实例就不会阻塞在这个位置
*/
public static LazySingleton getInstance() {
if (lazy == null) {
synchronized (LazySingleton.class) {
if (lazy == null) {
lazy = new LazySingleton();
}
}
}
return lazy;
}
}
但是不管是哪个方式来实现线程安全,采用synchronized来实现线程安全总是会对效率造成一定的影响。
三、饿汉式和懒汉式的结合
对于饿汉式和懒汉式都有相应的缺点,饿汉式可能造成内存浪费,懒汉式是线程不安全或者付出一定代价来实现线程安全,而采用静态内部类就可以将两种方式结合起来并解决掉这些缺点。
// 使用静态内部类,结合饿汉式到单例模式的特点,这样就不需要进行同步操作了
// 这种形式兼顾饿汉式单例模式的内存浪费问题和synchronized的性能问题
// 完美地解决这两个缺点
public class StaticInnerClassSingleton {
// 使用StaticInnerClassSingleton的时候,默认会先初始化内部类
// 如果没有使用则内部类不会加载
private StaticInnerClassSingleton(){}
public static final LazyInnerClassSingleton getInstance() {
return SingletonHolder.LAZY;
}
// 默认不加载,在调用的时候才会去进行加载
private static class SingletonHolder {
private static final StaticInnerClassSingleton LAZY = new StaticInnerClassSingleton();
}
}
四、破坏单例模式
破坏单例模式就是指使单例模式失效,创建出多个单例类的实例。
破坏单例模式的情况有两种,一是通过反射破坏单例模式,二是反序列化会破坏单例模式。
反射破坏单例模式:
// 反射破坏单例模式
public class LazySimpleSingletonTest {
public static void main(String[] args) {
// 通过反射破坏单例
try {
Class<?> clazz = StaticInnerClassSingleton.class;
// 通过反射获取构造方法
Constructor c= clazz.getDeclaredConstructor(null);
// 强制访问
c.setAccessible(true);
// 暴力初始化
Object o1 = c.newInstance();
Object o2 = c.newInstance();
System.out.println(o1 == o2); // 结果使false,表示两个实例不是同一个实例
} catch (Exception e) {
e.printStackTrace();
}
}
}
解决方法:
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton(){
// 防止使用反射破坏单例5
if (SingletonHolder.LAZY != null) {
throw new RuntimeException("不允许创建多个实例");
}
}
public static final LazyInnerClassSingleton getInstance() {
return SingletonHolder.LAZY;
}
private static class SingletonHolder {
private static final StaticInnerClassSingleton LAZY = new StaticInnerClassSingleton();
}
}
反序列化破坏单例模式:
// 创建一个新的单例类
public class SerializableSingleton implements Serializable {
private static final SerializableSingleton INSTANCE = new SerializableSingleton();
private SerializableSingleton(){}
public static SerializableSingleton getInstance() {
return INSTANCE;
}
}
// 反序列化破坏单例模式
public class SerializableSingletonTest {
public static void main(String[] args) {
SerializableSingleton s1 = null;
SerializableSingleton s2 = SerializableSingleton.getInstance();
FileOutputStream fos = null;
try {
// 序列化写入字节流
fos = new FileOutputStream("test.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
// 反序列化获取对象
FileInputStream fis = new FileInputStream("test.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SerializableSingleton) ois.readObject();
ois.close();
System.out.println(s1==s2); // 结果为false,表示不是同一个实例,原因好像是反序列化还原对象的时候会重新new一个对象(我不太清楚)
} catch (Exception e) {
e.printStackTrace();
}
}
}
解决方法:
public class SerializableSingleton implements Serializable {
private static final SerializableSingleton INSTANCE = new SerializableSingleton();
private SerializableSingleton(){}
public static SerializableSingleton getInstance() {
return INSTANCE;
}
// 防止序列化破坏单例,事实上还是创建了一个新的实例,只不过没有返回而已,而是通过该方法返回了已有的实例
public Object readResolve() {
return INSTANCE;
}
}
五、注册式单例模式
注册式单例模式是指将每一个实例都注册到一个容器中,使用唯一标识来获取实例。
使用Map来实现注册式单例模式:
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map<String,Object> ioc = new ConcurrentHashMap<>();
// 主要的做法是Map中有实例则返回,没有实例则创建实例放到容器中再返回
public static Object getBean(String className) {
synchronized (ioc) {
if (!ioc.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className).newInstance();
ioc.put(className,obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
} else {
return ioc.get(className);
}
}
}
}
总结
单例模式要确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。
另外主要是饿汉式单例模式和懒汉式单例模式的区别,对于注册式单例模式在spring框架中有用到。
饿汉式和懒汉式的最要区别:
- 饿汉式在类加载的时候就会初始化实例,而懒汉式在类加载的时候不会初始化实例,是在需要使用到该实例的时候才会初始化实例;
- 由于初始化的时间不同,饿汉式不管时候用到实例都会进行初始化而占用内存,可能会造成内存浪费,懒汉式就不存在该问题;
- 饿汉式天生线程安全,因为是在类加载的时候就初始化了,跟线程没有关系;而懒汉式是在使用的时候才会初始化,在多线程的情况下就可能创建出多个实例,如果需要线程安全则要通过加锁来完成;
- 可以通过静态内部类将两种方式结合起来并且解决各自的缺点而保留优点。
参考文章:
https://blog.csdn.net/jason0539/article/details/23297037
《SPRING5 核心原理和30个类手写实战》