文章目录
前言
单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。
例如ServletContext、ServletContextConfig在 Spring 框架应用中 ApplicationContext,数据库的连接池都是单例模式
单例模式
单例模式的应用
单例模式又分为饿汉式和懒汉式
饿汉式
饿汉式单例模式通过在类加载的时候初始化,通过static{}赋值,由于static{}只在类记载的时候调用一次,所以线程安全全局唯一.
例如
public class HungrySingle {
//先静态、后动态 //先属性、后方法 //先上后下
public static final HungrySingle single = new HungrySingle();
static {
System.out.println("static:"+single);
}
//避免正常创建方式
private HungrySingle () {
}
public static HungrySingle getSingle() {
return single;
}
}
上面这段代码虽然赋值语句并不在static{}中,但是通过javap -c -v查看编译后的class文件可以发现赋值依旧是在静态块中处理的,且一定在自定义的静态块内容之前处理
测试代码和结果
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(HungrySingle.getSingle());
}).start();
}
}
static:org.rule.single.HungrySingle@206cd157
org.rule.single.HungrySingle@206cd157
org.rule.single.HungrySingle@206cd157
org.rule.single.HungrySingle@206cd157
org.rule.single.HungrySingle@206cd157
org.rule.single.HungrySingle@206cd157
优点:没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好。
缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存.
懒汉式
通过双重检查锁确保全局唯一,代码如下:
public class LazySingle {
public static volatile LazySingle single = null;
//避免正常创建方式
private LazySingle() {
}
//添加局部变量,确保变量在被初始化后,每次方法执行时只被读取一次(LazySingle lazySingle = single;)
//因为字段添加了volatile关键字,确保每次读写都去内存中进行,这
//抛弃了CPU自带的高速多级缓存,使字段可以被在其它cpu中执行的线程
//察觉到变化.通过减少读取的次数,更多的利用到了高速多级缓存,使程序
//获得硬件上的速度提升
public static LazySingle getSingle() {
LazySingle lazySingle = single;
if (lazySingle == null) {
synchronized (LazySingle.class) {
if (single == null) {
single = lazySingle = new LazySingle();
}
}
}
return single;
}
}
测试代码
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(LazySingle.getSingle());
}).start();
}
结果
org.rule.single.LazySingle@55080cc6
org.rule.single.LazySingle@55080cc6
org.rule.single.LazySingle@55080cc6
org.rule.single.LazySingle@55080cc6
org.rule.single.LazySingle@55080cc6
双重检查锁:
第二个检查是因为虽然字段通过volatile修饰使得变量在线程间可见,但是在一开始获取的时候,可能多个线程携带的初始值都是null进入的方法,这样虽然通过synchronized确保了一次只能有一个线程执行,但是还是会创建多个对象.
虽然去掉第一个检查也能保证单例,但是增加了获取锁和释放锁的消耗,在CPU繁忙时可能导致大量的线程阻塞.
通过内部类初始化
//避免正常创建方式
private LazySingle() {
}
//确保不会被重写
public static final LazySingle getSingle1() {
return LazyHold.single;
}
private static class LazyHold {
public static final LazySingle single = new LazySingle();
}
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(LazySingle.getSingle1());
}).start();
}
结果
org.rule.single.LazySingle@7330523d
org.rule.single.LazySingle@7330523d
org.rule.single.LazySingle@7330523d
org.rule.single.LazySingle@7330523d
org.rule.single.LazySingle@7330523d
优点:可能兼顾了饿汉式的内存浪费,也避免了synchronized的性能消耗
缺点:多创建了类,虚拟机多加载了类(虚拟机不区分内部类或者外部类,都按正常类处理,只是存在一些变量作为标注),不一定避免了内存浪费.因为会创建新的Class对象到堆中.
IDEA下多线程的调试
通过设置断点挂起为线程级别,可以查看各个线程的内存状态
反射暴力攻击单例解决方案及原理分析
虽然构造方法通过设置私有访问符,确保了正常情况下不会被无意识破坏单例模式,但是可以通过反射调用的方式强制创建
public static void reflectCreate() {
try {
Constructor<LazySingle> defaultConstructor = LazySingle.class.getDeclaredConstructor(null);
defaultConstructor.setAccessible(true);
for (int i = 0; i < 3; i++) {
System.out.println(defaultConstructor.newInstance());
}
} catch (Exception e) {
e.printStackTrace();
}
}
结果
org.rule.single.LazySingle@1b6d3586
org.rule.single.LazySingle@4554617c
org.rule.single.LazySingle@74a14482
为了避免这种情况,可以通过在构造中抛异常的方式,当单例变量不为null时,再次访问抛出异常
//避免正常创建方式
private LazySingle() {
//添加判断,当尝试创建多个时,报错
if (LazyHold.single!=null){
throw new RuntimeException("实例只能唯一");
}
}
序列化破坏单例的原理及解决方案。
序列化破坏单例的原理
案例代码如下:
//反序列化时导致单例破坏
public class SerialSingle implements Serializable {
//序列化就是说把内存中的状态通过转换成字节码的形式
//从而转换一个 IO 流,写入到其他地方(可以是磁盘、网络 IO)
//将内存中状态给永久保存下来了
//反序列化
//将已经持久化的字节码内容,转换为 IO 流
//通过 IO 流的读取,进而将读取的内容转换为 Java 对象
//在转换过程中会重新创建对象 new
private static final SerialSingle single = new SerialSingle();
private SerialSingle() {
}
public static SerialSingle getSingle() {
return single;
}
/* private Object readResolve() {
return single;
}*/
}
public static void serial() {
SerialSingle s1 = null;
SerialSingle s2 = SerialSingle.getSingle();
try (
FileOutputStream fos = new FileOutputStream("serialSingle.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
FileInputStream fis = new FileInputStream("serialSingle.obj");
ObjectInputStream ois = new ObjectInputStream(fis);