单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创
建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供
了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
一、饿汉单例模式
单例设计模式分类两种:
饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
1.静态变量实现饿汉式单例模式
package 单例模式.饿汉式_静态成员变量;
/**
* @author Watching
* * @date 2023/3/14
* * Describe:饿汉模式:静态成员变量方式
*/
public class HungerSingleton {
//1.私有构造方法
private HungerSingleton() {}
//2.静态私有成员
private static HungerSingleton instance = new HungerSingleton();
//3.提供一个静态的方法获取唯一实例
public static HungerSingleton getInstance() {
return instance;
}
}
2.静态代码块实现饿汉式单例模式
package 单例模式.饿汉式_静态代码块;
/**
* @author Watching
* * @date 2023/3/14
* * Describe:饿汉式:静态代码块
*/
public class HungerSingleton {
//1.私有构造方法
private HungerSingleton() {}
//2.私有静态成员
private static HungerSingleton instance;
//3.静态代码块
static {
instance = new HungerSingleton();
}
//4.提供公有静态方法访问唯一对象
public static HungerSingleton getInstance() {
return instance;
}
}
3.最简单的饿汉式单例模式——枚举类
1.创建一个枚举类
public enum Singleton {
INSTANCE;
}
2.获取这个对象
public class Client {
public static void main(String[] args) {
Singleton instance1 = Singleton.INSTANCE;
Singleton instance2 = Singleton.INSTANCE;
System.out.println(instance1 == instance2);
}
}
两次获得对象是同一个对象
总结
饿汉式单例模式的三个要点
①私有构造方法
②私有静态成员
③直接初始化静态成员,或者在静态代码块中初始化
④提供静态方法获取单例
二、懒汉单例模式
1.静态内部类实现懒汉单例模式
1.使用静态内部类创建单例类
public class LazySingleton {
//1.创建一个私有构造器
private LazySingleton() {}
//2.创建一个静态内部类,并在其中初始化成员常量(变量也行)
static class SingletonHolder {
public static final LazySingleton instance = new LazySingleton();
}
//3.提供方法获取静态内部类中的成员常量(或者变量
public static LazySingleton getInstance() {
return SingletonHolder.instance;
}
}
2.测试
public class Client {
public static void main(String[] args) {
LazySingleton instance1 = LazySingleton.getInstance();
LazySingleton instance2 = LazySingleton.getInstance();
System.out.println(instance1 == instance2);
}
}
两次获取的对象都是同一个
为何静态内部类中的成员变量是单例的?
因为静态代码块只有在第一次被调用的时候才会加载。
静态内部类实现单例的优点
静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。
具体来说当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,
只有当getInstance()方法第一次被调用时,使用INSTANCE的时候,才会导致虚拟机加载SingleTonHoler类。
这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
那么他是如何实现线程安全的?
首先要了解类加载过程中的最后一个阶段:即类的初始化,类的初始化阶本质就是执行类构造器的方法。
方法:这不是由程序员写的程序,而是根据代码由javac编译器生成的。
它是由类里面所有的类变量的赋值动作和静态代码块组成的。
JVM内部会保证一个类的方法在多线程环境下被正确的加锁同步,也就是说如果多个线程同时去进行“类的初始化”,
那么只有一个线程会去执行类的方法,其他的线程都要阻塞等待,直到这个线程执行完方法。
然后执行完方法后,其他线程唤醒,但是不会再进入()方法。
也就是说同一个加载器下,一个类型只会初始化一次。
2.同步锁实现懒汉单例模式
为了保证懒汉式在多线程的环境中是线程安全的,只需要在基本的懒汉式的方法签名上加synchronized同步锁
public class LazySingleton {
private LazySingleton() {
}
private static LazySingleton instance;
//只需要在方法签名上添加一个synchronized关键字就可以保证该方法的线程安全性
public static synchronized LazySingleton getInstance() {
//判断instance是否为null 为null才创建,保证唯一性
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
这样可以保证并发中懒汉单例模式的线程安全,但是由于整个方法都被同步锁锁住,导致方法效率变低,每个线程的每次请求都会阻塞等待上一个线程判断是否存在单例。
3.双重检查锁实现懒汉式单例※
使用同步锁实现懒汉式单例模式导致性能下降,为了避免,我们是可以使用双重检查锁实现懒汉式单例模式。
public class LazySingleton {
private LazySingleton() {
}
// 1.添加volatile关键字后可以保证instance的有序性,不添加volatile关键字可能会出现空指针异常(由于jvm的指令重排)
private static volatile LazySingleton instance;
public static LazySingleton getInstance() {
//2.判断instance是否为null 为null就锁住后创建实例,保证安全性,双重检查锁解决了同步锁导致的多个线程进入需要排队取锁而影响性能的问题
if (instance == null) {
synchronized (LazySingleton.class){
if(instance == null){
instance = new LazySingleton();
}
}
}
return instance;
}
}
实现思路:是当一个线程已经进入instance = new LazySingleton();语句后,其他线程就要等待,如果当一个线程已经成功创建单例后,其余的线程进入就只会在第一条if语句处跳出,而不需要等待锁的释放。且之后的每条线程都会直接从第一个if语句处跳出,从而保证了线程安全,大大的提高了性能。
注意点:需要添加volatile关键字修饰静态成员变量,防止空指针异常。(详情请了解jvm的指令重排)
三、破坏单例模式与预防方法
1.反射破坏单例模式
思路:通过反射获取单例类的构造器,通过构造器创建实例
public class Client {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//1.获取单例类的类对象
Class<HungerSingleton> hungerSingletonClass = HungerSingleton.class;
//2.获取单例类的构造器并修改访问权限
Constructor<HungerSingleton> constructor = hungerSingletonClass.getDeclaredConstructor();
constructor.setAccessible(true);//设置访问权限,可以访问私有构造方法
//3.使用获取到的构造器创建实例
HungerSingleton hungerSingleton1 = constructor.newInstance();
HungerSingleton hungerSingleton2 = constructor.newInstance();
//4.测试发现两个实例不是同一个对象,单例被破坏了。
System.out.println(hungerSingleton1.toString());
System.out.println(hungerSingleton2.toString());
/**
* 两次对象结果的地址不同,说明他们不是同一个对象,单例被破坏了
*/
}
}
预防方法
在构造方法中判断是否已经创建单例,如果已经创建,则直接手动抛出异常
public class HungerSingleton {
//提供一个flag用于判断对象是否已经创建
private static boolean flag = false;
//1.私有构造方法
private HungerSingleton() {
//加锁保证多线程安全问题
synchronized (HungerSingleton.class){
//在无参构造中提供一个判断,如果已经创建了对象则抛出异常
if(flag){
throw new RuntimeException("禁止创建多个对象");
}
flag = true;
}
}
//2.私有静态成员
private static HungerSingleton instance;
//3.静态代码块
static {
instance = new HungerSingleton();
}
//4.提供公有静态方法访问唯一对象
public static HungerSingleton getInstance() {
return instance;
}
}
使用一个boolean类型的flag初始化为false,当flag为true是说明已经存在单例,再通过构造器创建实例就直接抛出异常。
第一次调用构造器即第一次创建单例会将flag置为true
测试一下
发现想利用反射创建两个实例,直接抛出了我们设置的异常。
2.反序列化破坏单例与预防方法
思路:将单例类序列化通过流存储在文件中,再通过反序列化将文件中的数据生成对象
public class Client {
public static void main(String[] args) throws IOException, ClassNotFoundException {
writeObjectToFile(HungerSingleton.getInstance());
readObjectFromFile();
readObjectFromFile();
/**
* 输出结果显示两个对象的地址不同,即代表两个对象不是同一个,这就破坏了单例模式了
*/
}
//将对象序列化并存入文件
public static void writeObjectToFile(HungerSingleton hungerSingleton) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:\\d.txt")));
oos.writeObject(hungerSingleton);
oos.close();
}
//将文件中的内容反序列化为对象
public static void readObjectFromFile() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:\\d.txt")));
HungerSingleton hungerSingleton = (HungerSingleton) ois.readObject();
System.out.println(hungerSingleton.toString());
}
}
此时两次反序列化生成的对象就是不同的两个对象。
注意:被序列化的类需要实现Serializable接口
预防方法
在单例类中添加一个readResolve()方法,在该方法内调用获取单例的静态方法
public class HungerSingleton implements Serializable {
//1.私有构造方法
private HungerSingleton() {}
//2.静态私有成员
private static HungerSingleton instance = new HungerSingleton();
//3.提供一个静态的方法获取唯一实例
public static HungerSingleton getInstance() {
return instance;
}
//4.提供readResolve()方法,保证反序列化为同一个对象
public Object readResolve(){
return HungerSingleton.getInstance();
}
}
在 ObjectInputStream 类中,会做一个判断,判断序列化的类中是否存在 readResolve() 方法,如果有,则会在反序列化的时候调用 readResolve() 方法。
有兴趣可以查看 ObjectInputStream 源码第2243行
四、jdk源码解析
在JDK中,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() {}
.............
}
可以看到Runtime也初始化了一个静态成员变量,和一个静态方法获取该成员变量,并且只有一个私有构造方法