单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
介绍
-
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
-
主要解决:一个全局使用的类频繁地创建与销毁。
-
何时使用:当您想控制实例数目,节省系统资源的时候。
-
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
-
关键代码:构造函数是私有的。
应用实例:
- Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
- 一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
优点:
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。
- 避免对资源的多重占用(比如写文件操作)。
缺点:
- 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景:
- 要求生产唯一序列号。
- WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
懒汉式
这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
- 优点:第一次调用才初始化,避免内存浪费。
- 缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。
import java.io.*;
/**
* 单例-懒汉模式
* 线程不安全,需要手动处理(双重锁)
* volatile,修饰成员变量,保证可见性与有效性
* synchronized:保证原子性
*
* 示例:ReactiveAdapterRegistry、TomcatURLStreamHandlerFactory
*
* new的数据都是放在内存中的
* jvm序列化就可以把内存中的数据通过序列化的机制把内存中的数据以某种格式 序列化出来
*
* @author liushiwei
*/
public class LazySingletonDemo {
public static void main(String[] args) {
LazySingleton instance = LazySingleton.getInstance();
// try {
// // 通过jdk 序列化到磁盘上 执行后会在当前项目下生成一个test文件
// ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test"));
// oos.writeObject(instance);
// } catch (IOException e) {
// e.printStackTrace();
// }
try {
// 通过jdk反序列化,反序列化会对单例造成破坏
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test"));
LazySingleton lazySingleton = (LazySingleton) ois.readObject();
System.out.println(lazySingleton == instance);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
/**
* 实现Serializable具备序列化的能力,如果类不实现serializable,就无法完成序列化和反序列化
*/
class LazySingleton implements Serializable {
/**
* 序列化版本
*/
private static final long serialVersionUID = -5849649956194247784L;
/**
* volatile:可以保证可见性与有效性
*/
private static volatile LazySingleton instance;
/**
* 私有的构造方法防止new实例
*/
private LazySingleton(){
}
public static LazySingleton getInstance() {
// 当前 实例为null是才创建
if(null == instance){
// 创建实例时加同步锁,保证线程安全
synchronized (LazySingleton.class){
// 防止多线程时,同时多个线程进入等待,一个线程完成创建后,其他线程再次new
if(null == instance){
// 内存模型
// new时会在堆(heap)开辟一个新空间,返回地址到临时内存区域(栈)
// 1.开辟新空间。
// 2.初始化空间
// 赋值
instance = new LazySingleton();
}
}
}
return instance;
}
/**
* 保证序列化一致性
* 需要指定 serialVersionUID 否则会抛serialVersionUID不一致无法序列化异常
* @return
* @throws ObjectStreamException
*/
Object readResolve() throws ObjectStreamException {
return getInstance();
}
}
饿汉式
这种方式比较常用,但容易产生垃圾对象。
- 优点:没有加锁,执行效率会提高。
- 缺点:类加载时就初始化,浪费内存。
它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* 饿汉模式
* 线程安全的(实例化的过程是线程安全的 ),因为初始化时时,同步进行的
* 典型示例:Runtime类
* @author liushiwei
*/
public class HungrySingletonDemo {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// HungrySingleton.getInstance();
// System.out.println("invoke HungrySingleton.getInstance()");
// 通过反射获取实例( 此时 HungrySingleton 类被实例化 )
Constructor<HungrySingleton> declaredConstructor = HungrySingleton.class.getDeclaredConstructor();
// 暴力反射
declaredConstructor.setAccessible(true);
// 创建一个实例 (会抛异常)
HungrySingleton hungrySingleton = declaredConstructor.newInstance();
// 懒汉模式获取实例
HungrySingleton instance = HungrySingleton.getInstance();
System.out.println(hungrySingleton == instance);
}
}
class HungrySingleton{
/**
* 初始化时机
* 1、当前类是启动类,即main函数所在类
* 2、直接进行new操作,访问静态属性,用反射访问类,初始化一个类的子类等操作
*
* 类加载的初始化阶段,完成了实例的初始化,借助jvm加载机制,保证实例的唯一性(初始化过程只会执行一次)
* 及线程安全(jvm以同步的形式来完成类加载的整个过程)
*
* 类加载过程
* 1、加载二进制数据到内存中,生成对应的Class数据结构
* 2、链接:a验证,b准备(给静态成员变量赋默认值),c解析
* 3、初始化:给类的静态变量赋初始值
*
* jvm中HungrySingleton只会被实例化一次,因为会存在缓存中
*/
private static HungrySingleton instance = new HungrySingleton();
private HungrySingleton(){
// 防止多例
if(null != instance){
throw new RuntimeException("单例不允许多个示例。");
}
}
public static HungrySingleton getInstance() {
return instance;
}
}
枚举
这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
/**
* 枚举-单例模式
* jdk提供的语法糖 继承 抽象类 java.long.enum
* 子类实例化的时候会调用父类进行实例化
* 类加载的时候就已经完成初始化
* 枚举是线程安全的
*
*/
public class EnumSingletoDemo {
public static void main(String[] args) {
EnumSingleton instance = EnumSingleton.INSTANCE;
instance.print();
EnumSingleton instance1 = EnumSingleton.INSTANCE;
System.out.println(instance1 == instance);
}
}
enum EnumSingleton{
INSTANCE;
public void print(){
System.out.println("111");
}
}