单例模式再次学习
一单例模式是什么?
单例模式是指在内存中只会创建且仅创建一次对象的设计模式(注意内部的成员以及方法用private修饰,访问权限仅限于类的内部)
二单例模式
1 饿汉式(一劳永逸)
在类加载时已经创建好该单例对象,等待被程序使用
package com.ma.singleton;
//饿汉式(一劳永逸)
//在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可
public class HungrySingleton {
private static HungrySingleton hungrySingleton=new HungrySingleton();
private HungrySingleton() { }
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。
2懒汉式 (临阵磨枪)
在真正需要使用对象时才去创建该单例类对象
package com.ma.singleton;
//懒汉式 (临阵磨枪)
public class lazybonesSingleton {
private static lazybonesSingleton lazybonesSingleton;
private lazybonesSingleton() { }
//程序使用对象前先进行判断 是否已经实例化(判空)如果没实例化则实例化,如果实例化则直接返回
public static lazybonesSingleton getInstance(){
if(lazybonesSingleton==null){
lazybonesSingleton=new lazybonesSingleton();
}
return lazybonesSingleton;
}
}
3 静态内部类
外部类 SingleTon加载时并不需要立即加载内部类SingleTonHoler,内部类不被加载则不去初始化INSTANCE,故而不占内存。能保证单例的唯一性,同时也延迟了单例的实例化。
public class SingleTon{
private SingleTon(){}
private static class SingleTonHoler{
private static SingleTon INSTANCE = new SingleTon();
}
public static SingleTon getInstance(){
return SingleTonHoler.INSTANCE;
}
}
4 枚举单例
枚举类不需要考虑关注线程安全、破坏单例和性能问题。
在程序启动时,会调用SingletonEnum的空参构造器,实例化好一个SingletonEnum对象赋给INSTANCE,之后再也不会实例化
package com.ma.singleton;
public enum SingletonEnum {
INSTANCE;
//构造方法私有化
public void anyMethod(){}
}
三单例模式分析
1懒汉式存在的问题
不适用于多线程,在多线程的模式下会创建多个单例对象。
原因是多线程的执行是默认是无序的,多个线程都是会执行到 **if(lazybonesSingleton==null)**当多个线程判断都是为空的时候,就会产生多个单例对象。
多线程先单例模式失败案例
生成多个单例对象
2解决多线程懒汉式失败的方法
2.1 DCL懒汉式
因为是多线程的情况下产生的,所以使用上锁的的方式去解决这个问题。
采用DCL懒汉式:Double Check(双重校验) + Lock(加锁)
package com.ma.singleton;
//懒汉式 (临阵磨枪)
public class LazybonesSingleton {
private static LazybonesSingleton lazybonesSingleton;
private LazybonesSingleton() {
System.out.println(Thread.currentThread().getName()+"创造LazybonesSingleton");
}
//程序使用对象前先进行判断 是否已经实例化(判空)如果没实例化则实例化,如果实例化则直接返回
public static LazybonesSingleton getInstance(){
if(lazybonesSingleton==null){// 多个线程同时看到lazybonesSingleton = null,第一个线程锁了资源
// 如果不为null,则直接返回lazybonesSingleton
synchronized (LazybonesSingleton.class){//上锁
if(lazybonesSingleton==null){//只会有一个线程进入 实例化对象
lazybonesSingleton=new LazybonesSingleton();
}
}
}
return lazybonesSingleton;
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(()->{
LazybonesSingleton.getInstance();
}).start();
}
}
}
2.2 加volatile防止指令重排
volatile的三大作用
1保证可见性
2 不保证原子性
3禁止指令重排(这里使用到)
使用volatile关键字修饰的变量,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变换。
当创建一个对象,在JVM中会经过三步:
(1)为singleton分配内存空间
(2)初始化singleton对象
(3)将singleton指向分配好的内存空间
而不加volatile则会导致2 、3 步的顺序发生颠倒,从而导致别的线程判断lazybonesSingleton已经不为空,但是确返回不了lazybonesSingleton对象
四破坏单例模式
4.1通过反射去破解单例
反射可破解前三种单例模式,但是不能破解枚举。
反射机制可以调用该私有构造器来创建对象。即便是在构造其中设置“防止多次实例化的代码”(比如设置一个flag)也于事无补,因为任然可以通过反射机制来修改flag,从而达到多次实例化的目的。
4.2 通过序列化去破解单例
前提是单例模式上实现接口Serializable,不实现接口的话,会出现 java.io.NotSerializableException的异常
代码案例
public class SingTest {
public static void main(String[] args) throws Exception {
//序列化
// 创建输出流
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("test.file"));
// 将单例对象写到文件中
oos.writeObject(LazybonesSingleton.getInstance());
oos.close();
//反序列化
// 从文件中读取单例对象
File file=new File("test.file");
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(file));
LazybonesSingleton instance=(LazybonesSingleton) ois.readObject();
// 判断是否是同一个对象
System.out.println(instance);
System.out.println(LazybonesSingleton.getInstance());
ois.close();
}
}