目录
概念
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。——黑马程序员
角色
单例类:只能创建一个实例的类。
访问类:使用单例类。
分类
饿汉式:类加载就会导致该单实例对象被创建。(加载就创建)
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建。(首次使用时才创建)
饿汉式
方式一(私有静态变量方式)
1、私有构造方法(私有的)
2、在本类中实例化本类对象(私有的,静态的)
3、提供一个公共的外部访问方式,让外界能够获取到该对象(共有的,静态的)
方式二(静态代码块方式)
1、私有构造方法(私有的)
2、声明该类类型的变量(并不赋值,NULL)
3、在静态代码块中赋值
4、提供一个公共的外部访问方式,让外界能够获取到该对象(共有的,静态的)
缺点
对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。
懒汉式
方式一(线程不安全方式)
1、私有构造方法(私有的)
2、声明该类类型的变量(并不赋值,NULL)
3、提供一个公共的外部访问方式,让外界能够获取到该对象,并对声明的类型进行判断,如果为NULL则实例化对象,如果已经赋值则直接返回。(公有的,静态的)
缺点
在多线程的程序中可能会创建多个对象。
例如:有线程x1,x2
当x1刚执行结束if语句时,x2拿到cpu的执行权。
此时x1还没有创建对象,所以x2的if判断也为空。
当轮到x1获得cpu执行权时x1会创建对象,x2会卡在创建对象的语句不动,轮到x2执行时,x2也会创建对象。
方式二(线程安全方式)
1、私有构造方法(私有的)
2、声明该类类型的变量(并不赋值,NULL)
3、提供一个公共的外部访问方式,让外界能够获取到该对象,并对声明的变量进行判断,如果为NULL则实例化对象,如果已经赋值则直接返回。(公有的,加锁的(synchronized),静态的)
与线程不安全的方式区别就在于:线程安全方式提供的外部访问的方法上锁了,这样就能够避免子在多个线程中可能会出现多个线程创建了对象的问题。
缺点
每次方位都需要加锁访问,访问的速度会变得慢。
方式三(双重检查锁)
1、私有构造方法(私有的)
2、声明该类类型的变量(并不赋值,NULL)
3、提供一个公共的外部访问方式,让外界能够获取到该对象,并对声明的变量进行判断,如果为NULL则上锁,并且再一次判断对象是否为空,若还是为空则实例化对象,如果已经赋值则直接返回。(公有的,加锁的(synchronized),静态的)
缺点
在多线程的情况下,有可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
要解决双重检查锁模式带来空指针异常的问题,只需要使用volatile关键字,volatile 关键字可以保证可见性和有序性。
volatile关键字在声明该类类型时修饰。
1、私有构造方法(私有的)
2、声明该类类型的变量(volatile,并不赋值,NULL)
3、提供一个公共的外部访问方式,让外界能够获取到该对象,并对声明的变量进行判断,如果为NULL则上锁,并且再一次判断对象是否为空,若还是为空则实例化对象,如果已经赋值则直接返回。(公有的(public),加锁的(synchronized),静态的(static))
方式四(静态内部类方式)
静态内部方式中,实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。——黑马程序员
1、私有构造方法(私有的)
2、定义一个私有的静态内部类。(在该类内部实例化一个不变的(final),私有的(private),静态的(static)外部类对象)
3、提供一个公共的外部访问方式,让外界能够获取到该对象,并对声明的变量进行判断,如果为NULL则上锁,并且再一次判断对象是否为空,若还是为空则实例化对象,如果已经赋值则直接返回。(公有的(public),静态的(static))
说明
第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。——黑马程序员
小结
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。——黑马程序员
枚举类型
1、创建一个枚举类型
2、写一个自己
额,这个我也不知道咋说,直接上代码吧。
/**
* 枚举方式
*/
public enum Singleton {
INSTANCE;
}
//你没有看错,就是这么简单
然后在测试类的主方法中以Singleton.INSTANCE创建创建两个Singleton对象并打印出它们进行“=”运算的结果。
说明
枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
枚举类型是属于饿汉式的创建模式,在不考虑内存空间浪费的情况下可以使用枚举类型的创建方式。
存在的问题
破坏单例模式
除了枚举(在JVM底层已经处理好了)的方式,以上的方法可以适用序列化和反射破坏单例模式。
序列化方式破坏单例模式
主方法:
1、调用文件输出方法。
2、新建两个Singleton对象从文件中获取Singleton对象
3、打印将两个对象比较后的结果。
文件读取方法:
1、创建对象输入流对象。
2、读取以文件输出方法输出的Singleton对象。
文件输出方法:
1、获取Singleton对象。
2、创建对象输出流,设置一个文本文件的输出路径。
3、将获取到的Singleton对象以对象输出流的方式输出。
注意:Singleton对象需要实现序列化接口(Serializable),否则在文件输出的方法中是无法进行输出的。
解决方式
在单例类中新增加一个readResolve方法,在方法内部返回该类中的单例即可。因为当进行反序列化时,会自动调用该方法,将方法的返回值直接返回。
主要的原因:在readObject方法中,底层的实现原理是去判断在该类中是否有存在readResolve方法,如果存在,则调用该方法,如果不存在就会重新new出一个。
反射方式破坏单例模式
1、获取Singleton的字节码对象。
2、获取无参构造方法对象。(适用字节码获取私有的无参构造方法对象的方法:getDeclaredConstructor)
3、取消访问检查
4、创建两个Singleton对象。(使用构造对象中的newInstance方法)
5、打印两个Singleton对象的比较结果。
解决方式
在单例的无参构造方法中做一个判断,如果属性中的那个单例对象以及被实例化过,不等于null了,那么说明这个单例已经被创建过了,抛出一个异常,如果判断的结果为null,那么说明这是第一次创建这个单例,那么就正常创建。这种方式在有多线程的情况下可能会有线程的问题,所以要加上锁。
如果在单例类中没有创建属性而是使用内部类的方式创建的,那么也只需要再创建一个boolean类型的变量用于标记是否是第一次创建的即可。
JDK源码解析-Runtime类
可以看出这是很经典的单例模式的饿汉式(静态属性)
Runtime中的exec()使用示例
package com.stu.algorithm;
import java.io.IOException;
import java.io.InputStream;
/**
* @version:
* @author: 零乘一
* @description: 类的简介
* @date: 2021/10/2 23:00
**/
public class RuntimeDemo {
public static void main(String[] args) throws IOException {
Runtime runtime = Runtime.getRuntime();
Process ipConfig = runtime.exec("ipconfig");
InputStream is = ipConfig.getInputStream();
byte[] bytes = new byte[1024 * 1024 * 100];
int len = is.read(bytes);
System.out.println(new String(bytes, 0, len, "GBK"));
}
}
运行结果
在控制台输出的内容与cmd中输入ipconfig的内容在小黑窗输出的内容想相同。自行试试就能看到效果。