设计模式——单例模式
单列模式
采用一定的方法保证在整个软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个能够取得其对象实例的静态方法
简单来说就是,这个类在自己内部实例化了自己,外部不能实例化(通过构造函数私有化实现),只能通过该类的内部静态方法来获取到该类的实例化对象。
由于这个类是在自己类的内部实例化的,该里只加载了一次,所以这个类的对象也只实例化了一次,无论外部调用多少次该内部的静态类方法,获取到的都是同一个对象实例
单例设计模式分类两种:
饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
饿汉式
public class Singleton {
//私有构造方法
private Singleton() {
}
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
说明:
饿汉式里,instance会随着类的加载而加载。如果该对象足够大,而且一直没有被使用的话,就会造成内存的浪费
懒汉式
方式一:线程不安全
/**
* 懒汉式
* 线程不安全
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
说明:
当外部调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。
在多线程的情况下,如果多个线程同时进入到了判断的if语句,都判断了instance为null,那么他们就都可以创建对象了,违背了单例模式
方式二:线程安全
/**
* 懒汉式
* 线程安全
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
//对外提供静态方法获取该对象
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
说明:
该方式不仅实现了懒加载的效果,还解决了线程安全的问题。
但是在方法上加上了synchronized关键字,导致该方法的执行效率特别低
在多线程的环境下,只有一个线程能够获取锁进入方法里实例化对象。但当对象实例化后会释放锁,提供其他线程使用。
因为instance已经被第一个获取到锁的线程给赋值了,所以后面来的线程执行的其实都是读操作,调用方法后就直接return instance读取instance对象,不需要也不能够再次赋值。而读操作本身就是不存在线程安全问题的。而由于我们在方法上加了synchronized关键字,在instance被一次赋值后,后面来的线程来读取instance的时候,每次只能由一个线程读取到,这是十分低效的。
方式三:双重检查锁
/**
* 双重检查方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
if(instance == null) {
synchronized (Singleton.class) {
//抢到锁之后再次判断是否为null
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile
关键字, volatile
关键字可以保证可见性和有序性。
/**
* 双重检查方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
if(instance == null) {
synchronized (Singleton.class) {
//抢到锁之后再次判断是否为空
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
小结:
添加 volatile
关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。
懒汉式-方式4(静态内部类方式)
静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被 static
修饰,保证只被实例化一次,并且严格保证实例化顺序。
/**
* 静态内部类方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
说明:
第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder
并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。
小结:
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
枚举方式
枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
/**
* 枚举方式
*/
public enum Singleton {
INSTANCE;
}
使用:
Singleton instance=Singleton.INSTANCE;
Singleton instance1=Singleton.INSTANCE;
System.out.println(instance==instance1); //true
说明:
枚举方式属于恶汉式方式。
存在的问题
反序列化破坏单例模式
序列化是将对象转换为可存储或传输的格式的过程,而反序列化是将存储或传输的数据转换回对象的过程。
由于反序列化过程中,调用的并不是我们写的类中的
getInstance()
方法,会在底层调用readResolve()
方法方法来new对象,所以反序列化两次获取到的对象都是不一样的
破坏演示:
单例类
要实现Serializable接口才能实习序列化和反序列化
public class Singleton implements Serializable {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
Test类
public class Test {
public static void main(String[] args) throws Exception {
//往文件中写对象
//writeObject2File();
//从文件中读取对象
Singleton s1 = readObjectFromFile();
Singleton s2 = readObjectFromFile();
//判断两个反序列化后的对象是否是同一个对象
System.out.println(s1 == s2);
}
private static Singleton readObjectFromFile() throws Exception {
//创建对象输入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\Think\\Desktop\\a.txt"));
//第一个读取Singleton对象
Singleton instance = (Singleton) ois.readObject();
return instance;
}
public static void writeObject2File() throws Exception {
//获取Singleton类的对象
Singleton instance = Singleton.getInstance();
//创建对象输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\Think\\Desktop\\a.txt"));
//将instance对象写出到文件中
oos.writeObject(instance);
}
}
解决方法:
既然反序列破坏单例模式的原理是,调用反序列化自带的readResolve()
方法,那么我们将该方法重写就可以避免反序列化创建新对象了
public class Singleton implements Serializable {
//私有构造方法
private Singleton() {}
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
/**
* 下面是为了解决序列化反序列化破解单例模式
*/
private Object readResolve() {
return SingletonHolder.INSTANCE;
}
}
反射破坏单列模式
在Java中,可以通过反射访问并且修改类的所有字段和方法以及构造函数,从而达到破坏单例模式的效果。
通过反射,把单例模式中的私有构造方法变为共有的,这样在外部就可以多次new 对象了,这样破坏了单例模式
因为反射可以修改类中的大部分信息,所以无法完全阻止反射对单例模式的破坏。只能在一定程度上增强单例模式的安全性。
破坏演示
public class Test {
public static void main(String[] args) throws Exception {
//获取Singleton类的字节码对象
Class clazz = Singleton.class;
//获取Singleton类的私有无参构造方法对象
Constructor constructor = clazz.getDeclaredConstructor();
//取消访问检查
constructor.setAccessible(true);
//创建Singleton类的对象s1
Singleton s1 = (Singleton) constructor.newInstance();
//创建Singleton类的对象s2
Singleton s2 = (Singleton) constructor.newInstance();
//判断通过反射创建的两个Singleton对象是否是同一个对象
System.out.println(s1 == s2);
}
}
解决方法
在构造函数中判断,对象实例是否已经创建了,如果创建了,那就报错,不让他们重复创建对象
因为反射可以修改类中的大部分信息,所以无法完全阻止反射对单例模式的破坏。只能在一定程度上增强单例模式的安全性。
就算是下面这种方法,也有被反射破坏的风险,因为可以通过反射来修改flag的值,让他一直为true
package 单例模式;
public class Singleton {
private static boolean flag = false;
//私有构造方法
private Singleton() {
synchronized (Singleton.class) { //防止多线程下出现同时读取到的flag为false
/*
反射破解单例模式需要添加的代码
*/
if (flag) {
throw new RuntimeException("不能创建多个对象");
}
flag = true;
}
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
单例模式的应用
实际上,很多程序,尤其是Web程序,大部分服务类都应该被视作Singleton,如果全部按Singleton的写法写,会非常麻烦,所以,通常是通过约定让框架(例如Spring)来实例化这些类,保证只有一个实例,调用方自觉通过框架获取实例而不是new
操作符:
@Component // 表示一个单例组件
public class MyService {
...
}
因此,除非确有必要,否则Singleton模式一般以“约定”为主,不会刻意实现它。
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类使用的是恶汉式(静态属性)方式来实现单例模式的。