Appearance
单例模式
单例模式(singleton Pattern)是java中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式。可以直接访问,不需要实例化该类的对象
单例模式的结构
单例模式主要有一下角色:
- 单例类:只能创建一个实例的类
- 访问类:使用单例类
单例模式的实现
单例设计模式分类有两种:
- 饿汉式:类加载就会导致该单实例对象被创建
- 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会被创建
饿汉式-方式1(静态变量方式)
java
package com.huangjiliang.design.heima.single;
/**
* 单例模式-饿汉式-(静态变量方式)
*/
public class Singleton1 {
private Singleton1() {}
private static Singleton1 instance = new Singleton1();
public static Singleton1 getInstance() {
return instance;
}
}
- 该方式再成员位置生命Singleton1类型的静态变量,并创建Singleton1类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费
饿汉式-方式2(静态代码块方式)
java
package com.huangjiliang.design.heima.single;
/**
* 单例模式-饿汉式(静态代码块方式)
*/
public class Singleton2 {
private Singleton2() {}
private static Singleton2 instance;
static {
instance = new Singleton2();
}
private Singleton2 getInstance() {
return instance;
}
}
- 该方式再成员位置生命Singleton2类型的静态变量,而对象的创建时再静态代码块中,也是对着类的加载而创建。所以和饿汉式的静态变量方式基本上一样,即也会存在内存浪费的问题
懒汉式-方式1(线程不安全)
java
package com.huangjiliang.design.heima.single;
/**
* 单例模式-懒汉式(线程不安全)
*/
public class Singleton3 {
private Singleton3(){}
private static Singleton3 instance;
public static Singleton3 getInstance() {
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}
- 从上面代码我们可以看出该方式再成员位置声明Singleton3类型的静态变量,并没有进行对象的复制操作,那么什么时候赋值的呢?当调用getInstance()方法获取Singleton3类的对象的时候才会创建Singleton3类的对象,这样就实现了懒加载的效果。但是,如果是在多线程环境,会出现线程安全问题
懒汉式-方式2(线程安全)
java
package com.huangjiliang.design.heima.single;
/**
* 单例模式-懒汉式(线程安全)
*/
public class Singleton4 {
private Singleton4(){}
private static Singleton4 instance;
/**
* 加所
* @return
*/
public static synchronized Singleton4 getInstance() {
if (instance == null) {
instance = new Singleton4();
}
return instance;
}
}
- 该方式也实现了懒加载的效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了
懒汉式-方式3(双重检查锁)
针对懒汉式-方式2(线程安全)加锁导致执行效果效率低问题,因为对于getInstamce()方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必要让每个线程都必须持有锁才能调用该方法,我们需要调整枷锁的时机,来提高获取对象的效率。由此也产生了一种新的实现模式:双重检查锁模式
java
package com.huangjiliang.design.heima.single;
/**
* 单例模式-懒汉式(线程安全-双剑检查机制-提高效率)
*/
public class Singleton5 {
private Singleton5(){}
private static Singleton5 instance;
/**
* 加所
* @return
*/
public static Singleton5 getInstance() {
// 第一次判断instance是否为null,不进行加锁操作,直接返回实例
if (instance == null) {
synchronized (Singleton5.class) {
// 抢到锁之后再次判断是否为null
if (instance == null) {
instance = new Singleton5();
}
}
}
return instance;
}
}
- 双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去是完美无缺的,其实是存在问题,在多线程情况下,可能会出现空指针问题。出现问题的原因是JVM在实例化对象的时候进行优化和指令重排序操作
- 要解决双重检查锁模式带来空指针异常的问题,只需要使用volatile关键字,volatile关键字可以保证可见性和有序性
java
package com.huangjiliang.design.heima.single;
/**
* 单例模式-懒汉式(线程安全-双剑检查机制-提高效率-保证可见性和有序性)
*/
public class Singleton6 {
private Singleton6(){}
// 增加volatile保证可见性和有序性
private static volatile Singleton6 instance;
/**
* 加所
* @return
*/
public static Singleton6 getInstance() {
// 第一次判断instance是否为null,不进行加锁操作,直接返回实例
if (instance == null) {
synchronized (Singleton6.class) {
// 抢到锁之后再次判断是否为null
if (instance == null) {
instance = new Singleton6();
}
}
}
return instance;
}
}
- 添加volatile关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证多线程的情况下线程安全也不会有性能问题
懒汉式-方式4(静态内部类方式)
静态内部类单例模式中实例由内部类创建,由于JVM在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性、方法被调用的时候才会被加载,并初始化其静态属性。静态属性由于被static修饰,保证只被实例化一次,并且严格保证实例化顺序
java
package com.huangjiliang.design.heima.single;
/**
* 单例模式-静态内部类方式
*/
public class Singleton7 {
private Singleton7() {}
private static class SingletonHolder {
private static final Singleton7 INSTANCE = new Singleton7();
}
// 对外提供静态方法获取该对象实例
public static Singleton7 getInstance() {
return SingletonHolder.INSTANCE;
}
}
- 第一次加载Singleton7类时是不会去初始化INSTANCE,只有第一次调用getInstance()方法,虚拟机才会加载内部类SingletonHolder,并初始化INSTANCE,这样不仅确保线程安全,也能保证Singleton7类的唯一性
- 静态内部类单例模式时一种优秀的单例模式,时开源项目中比较常用的单例模式,在没有任何加锁的情况下,保证了多线程的安全,并且没有任何性能影响和空间的浪费
枚举方式
枚举类实现单例模式时极力推荐的单例实现模式,因为枚举类是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类时所有单例实现中唯一一种不会被破坏的单例实现模式
java
package com.huangjiliang.design.heima.single;
/**
* 单例模式 - 枚举类方式
*/
public enum Singleton {
INSTANCE;
}
- 说明:枚举方式属于饿汉方式
存在问题
破坏单例模式
使上面定义的单例类可以创建多个对象,枚举类除外。有两种方式,分别是序列化和反射
问题复现
序列化和反序列化
java
package com.huangjiliang.design.heima.single;
import java.io.Serializable;
/**
* 单例模式-静态内部类
*/
public class Demo1 implements Serializable {
private Demo1(){}
public static Demo1 getInstance() {
return SingleHolder.instance;
}
private static class SingleHolder {
private static Demo1 instance = new Demo1();
}
}
java
package com.huangjiliang.design.heima.single;
import java.io.*;
/**
* 演示序列化和反序列化破坏Demo1类单例模式(静态内部类方式)
*/
public class Demo1Test {
private static String filePath = "D:\\workSpace\\vitepress-notes\\docs\\other\\design\\create\\test.txt";
public static void main(String[] args) throws Exception {
// writeObject2File();
Demo1 demo1 = readObjectFromFile();
Demo1 demo11 = readObjectFromFile();
// 两次获取实例对象,比对,发现输出false,证明两个实例对象不同,违反了单例模式
System.out.println(demo11 == demo1);
}
public static Demo1 readObjectFromFile() throws IOException, ClassNotFoundException {
// 创建对象输入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath));
// 读取出对象
Demo1 instance = (Demo1)ois.readObject();
return instance;
}
/**
* 将对象写到文件中
* @throws IOException
*/
public static void writeObject2File() throws IOException {
// 获取Demo1类的对象
Demo1 instance = Demo1.getInstance();
// 创建对象输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));
// 将对象写到文件中
oos.writeObject(instance);
}
}
- 两次从文件中获取实例对象,比对,发现输出false,证明两个实例对象不同,违反了单例模式
反射
java
package com.huangjiliang.design.heima.single;
/**
* 创建型模式之单例模式
* 双重检查
*/
public class Demo2 {
private Demo2() {}
private static volatile Demo2 instance;
public static Demo2 getInstance() {
if (instance != null) {
return instance;
}
synchronized (Demo2.class) {
if (instance != null) {
return instance;
}
instance = new Demo2();
return instance;
}
}
}
java
package com.huangjiliang.design.heima.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* 使用反射机制演示,破坏Demo2单例模式
*/
public class Demo2Test {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// 获取Demo2类的字节码对象
Class<Demo2> demo2Class = Demo2.class;
// 获取Demo2类的私有无参构造方法对象
Constructor<Demo2> declaredConstructor = demo2Class.getDeclaredConstructor();
// 取消访问检查
declaredConstructor.setAccessible(true);
// 创建对象
Demo2 demo2 = declaredConstructor.newInstance();
// 创建对象
Demo2 demo21 = declaredConstructor.newInstance();
// 判断通过反射创建的两个Demo2对象是否同一个对象。输出false
System.out.println(demo21 == demo2);
}
}
- 上面代码运行结果是false,表示可以使用反射机制可以破坏单例模式
问题解决方案
序列化、反序列化破坏单例模式的解决方法
在Demo1类中添加readResolve方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的返回值,如果没有定义,则返回新new出来的对象
java
package com.huangjiliang.design.heima.single;
import java.io.Serializable;
/**
* 单例模式-静态内部类
*/
public class Demo1 implements Serializable {
private Demo1(){}
public static Demo1 getInstance() {
return SingletonHolder.instance;
}
private static class SingletonHolder {
private static Demo1 instance = new Demo1();
}
/**
* 下面是为了解决序列化反序列化破解单例模式
*/
private Object readResolve() {
return SingletonHolder.instance;
}
}
原理分析《ObjectInputstream类》
java
public final Object readObject() throws IOException, ClassNotFoundException{
...
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false);//重点查看readObject0方法
.....
}
private Object readObject0(boolean unshared) throws IOException {
...
try {
switch (tc) {
...
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));//重点查看readOrdinaryObject方法
...
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
private Object readOrdinaryObject(boolean unshared) throws IOException {
...
//isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类,
obj = desc.isInstantiable() ? desc.newInstance() : null;
...
// 在Singleton类中添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true
if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
// 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量
// 这样多次调用ObjectInputStream类中的readObject方法,继而就会调用我们定义的readResolve方法,所以返回的是同一个对象。
Object rep = desc.invokeReadResolve(obj);
...
}
return obj;
}
反射方式破解单例的解决方法
java
package com.huangjiliang.design.heima.single;
/**
* 创建型模式之单例模式
* 双重检查
*/
public class Demo2 {
private Demo2() {
/**
* 反射破解单例模式需要添加的代码
*/
if(instance != null) {
throw new RuntimeException();
}
}
private static volatile Demo2 instance;
public static Demo2 getInstance() {
if (instance != null) {
return instance;
}
synchronized (Demo2.class) {
if (instance != null) {
return instance;
}
instance = new Demo2();
return instance;
}
}
}
- 这种方法比较好理解。当通过反射方式调用构造函数方法进行创建时,直接抛出异常,不运行此中操作
JDK单例解析-Runtime类
Runtime类就是使用的单例设计模式
1、通过源代码查看使用的时哪几种单例模式
java
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类使用的是恶汉式(静态属性)方式来实现单例模式的。
2、使用Runtime类中的方法
java
public class RuntimeDemo {
public static void main(String[] args) throws IOException {
//获取Runtime类对象
Runtime runtime = Runtime.getRuntime();
//返回 Java 虚拟机中的内存总量。
System.out.println(runtime.totalMemory());
//返回 Java 虚拟机试图使用的最大内存量。
System.out.println(runtime.maxMemory());
//创建一个新的进程执行指定的字符串命令,返回进程对象
Process process = runtime.exec("ipconfig");
//获取命令执行后的结果,通过输入流获取
InputStream inputStream = process.getInputStream();
byte[] arr = new byte[1024 * 1024* 100];
int b = inputStream.read(arr);
System.out.println(new String(arr,0,b,"gbk"));
}
}