单例模式

单例模式:确保一个类在任何情况下都是绝对只有一个实例

1.饿汉式单例模式

  • 饿汉式单例模式在类加载的时候就初始化,并创建单例对象,绝对的线程安全,在线程还没有出现之前就实例化了,不可能存在线程安全问题
  • 优点
    没有加任何锁,执行效率较高,用户体验比懒汉式单例模式更好
  • 缺点
    类加载的时候就初始化,始终占用空间,浪费内存
    代码:
public class HungrySingleton {
    private static final HungrySingleton hSingleton = new HungrySingleton();
    public static HungrySingleton getInstance(){
        return hSingleton;
    }
}

或者利用静态代码块

public class HungrySingleton {
   // private static final HungrySingleton hSingleton = new HungrySingleton();
   private static final HungrySingleton hSingleton;
   static {
       hSingleton=new HungrySingleton();
   }
    public static HungrySingleton getInstance(){
        return hSingleton;
    }
}

2. 懒汉式单例模式

  • 只有在被外部类调用的时候内部类才会被加载
    代码实现
public class LazySimpleSingleton {
    private static LazySimpleSingleton lazySimpleSingleton =null;
    public static LazySimpleSingleton getInstance(){
        if (lazySimpleSingleton==null){
            lazySimpleSingleton = new LazySimpleSingleton();
        }
        return lazySimpleSingleton;
    }
}

通过两个线程测试懒汉式单例模式

public class TestThread implements Runnable {
    @Override
    public void run() {
        LazySimpleSingleton simpleSingleton = LazySimpleSingleton.getInstance();
        System.out.println(Thread.currentThread().getName()+":"+simpleSingleton);
    }
}
public class Test {
    public static void main(String[] args) {
        Thread t1 = new Thread(new TestThread());
        Thread t2 = new Thread(new TestThread());
        t1.start();
        t2.start();
        System.out.println("End");
    }
}

结果可能会出现不一样的实例:也就意味着上述单例模式存在线程安全隐患
在这里插入图片描述
解决办法:

  • 通过使用synchronize关键字,但是上锁,对程序性能还是存在一定的影响
  • 方法二
    代码如下
public class LazyInnerClassSingleton {
    public static final LazyInnerClassSingleton getInstance(){
        return LazyHolder.LAZY ;
    }
    private static class LazyHolder{
    // static:单例的空间共享,保证这个方法不会被重写,重载
        public static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}
  • 这种方式兼顾饿汉式单例模式的内存问题和synchronize的性能问题
  • 没有使用时,内部类是不加载的,只有当方法被调用才会调用内部类,实例化单例

3. 反射破坏单例

上述代码貌似很牛皮,下面用反射方式破坏上述懒汉式单例模式

 public static void main(String[] args) throws Exception {
        Class<?> clazz = LazyInnerClassSingleton.class;
        //反射获取构造器
       Constructor constructor = clazz.getDeclaredConstructor(null);
       //强制访问
       constructor.setAccessible(true);
       //暴力初始化两次,相当于new了两次
       Object object1 = constructor.newInstance();
       Object object2 = constructor.newInstance();
       System.out.println(object1==object2);
    }
}

结果为false,并没能保证单例模式
原因可见构造这边出了问题,对构造器进行优化

public class LazyInnerClassSingleton {
    public LazyInnerClassSingleton(){
        if (LazyHolder.LAZY!=null){
            throw new RuntimeException("不允许创建多个实例!");
        }
    }
    public static final LazyInnerClassSingleton getInstance(){
        return null;
    }
    private static class LazyHolder{
        public static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

在运行上面的main方法,结果如下
在这里插入图片描述
此时,内部类方式的单例模式大功告成

你以为到此结束了么,不还有

4.序列化破坏单例

  • 一个单例对象创建好后,若将对象序列化写入磁盘,下次使用再取出来反序列化,将其转化为内存对象,反序列化的对象会重新分配内存,即重新创建,违背了单例模式的初衷
    代码复现上述问题
public class SerializableSingleton implements Serializable {
    public final static SerializableSingleton instance = new SerializableSingleton();
    public static SerializableSingleton getInstance(){
        return instance;
    }
}

测试代码:

public static void main(String[] args) throws Exception{
        SerializableSingleton s1 =null;
        SerializableSingleton s2 = SerializableSingleton.getInstance();
        //写
        FileOutputStream fileOutputStream = null;
        fileOutputStream = new FileOutputStream("SerializableSingleton.obj");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(s2);
        objectOutputStream.flush();
        objectOutputStream.close();
		//读
        FileInputStream fileInputStream = new FileInputStream("SerializableSingleton.obj");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        s1 = (SerializableSingleton) objectInputStream.readObject();
        objectInputStream.close();
        //比较
        System.out.println(s1.toString());
        System.out.println(s2.toString());
        System.out.println(s1==s2);
    }

结果:
在这里插入图片描述
注:序列化就是把内存中的状态通过转换成字节码形式,从而转换成一个I/O流,写入到其他地方(磁盘,网络I/O),内存中的状态会永久保存
反序列化,将已经持久化的字节码内容转换成I/O流,通过I/O流读取,进而将读取的内容转换成java对象,在转换过程中会重新创建对象

解决方式:修改SerializableSingleton (添加readResolve())

public class SerializableSingleton implements Serializable {
    public final static SerializableSingleton instance = new SerializableSingleton();
    public static SerializableSingleton getInstance(){
        return instance;
    }
    private Object readResolve(){
        return instance;
    }
}

再次测试上述代码,结果如下:
在这里插入图片描述
为什么?
我也不知道,一起来看一下源码
ObjectInputStream==>readObject() 调用了readObject0(false)方法

Object obj = readObject0(false);

进入readObject0( )方法==>readOrdinaryObject(unshared)

case TC_OBJECT:
      return checkResolve(readOrdinaryObject(unshared));

readOrdinaryObject()中,调用isInstantiable()方法,判断一下构造函数是否不为null,意味着只要有无参构造方法就会被实例化(一个类默认有一个无参构造方法,无覆盖前提下)

继续往下走:
readOrdinaryObject()调用isInstantiable()方法后,又调用了hasReadResolveMethod()

boolean hasReadResolveMethod() {
        requireInitialized();
        return (readResolveMethod != null);
    }

这个方法很简单,判断readResolveMethod 是否为null,在 ObjectStreamClass类中,私有ObjectStreamClass方法,对参数readResolveMethod 赋值

private ObjectStreamClass(final Class<?> cl) {
 ...
 readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);
 ...
 }

上面逻辑就是通过反射找到一个无参的readResolve()方法,并保存起来
如果hasReadResolveMethod方法返回true,调用invokeReadResolve()方法,利用反射调用invokeReadResolve()方法

 return readResolveMethod.invoke(obj, (Object[]) null);

总结:虽然通过增加了readResolve()方法,解决了单例被破坏的问题,但实际上还是实例化了两次,只不过新创建的对象没有被返回而已

针对上述问题进一步改进

5. 注册式单例模式

5.1 枚举式单例模式

  • 直接上代码
public enum  EnumSingleton {
    INSTANCE;
    private Object data;
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}
  • 测试
public static void main(String[] args) throws Exception{
        EnumSingleton instance1 =null;
        EnumSingleton instance2 = EnumSingleton.getInstance();
        instance2.setData(new Object());
        //写
        FileOutputStream fileOutputStream = null;
        fileOutputStream = new FileOutputStream("EnumSingleton.obj");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(instance2);
        objectOutputStream.flush();
        objectOutputStream.close();
        //读
        FileInputStream fileInputStream = new FileInputStream("EnumSingleton.obj");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        instance1 = (EnumSingleton) objectInputStream.readObject();
        objectInputStream.close();
        //比较
        System.out.println(instance1.getData());
        System.out.println(instance2.getData());
        System.out.println(instance1.getData()==instance2.getData());
    }
  • 结果:
    在这里插入图片描述
  • 枚举式单例模式在静态代码块中就给 instance 进行了赋值,是饿汉式单例模式的实现
static{	
	INSTANCE = new EnumSingleton("INSTANCE",0);
	$VALUES = (new EnumSingleton[] {
		INSTANCE
	});
}
  • 能不能通过序列化破坏呢?
    答案是不会的,在readObject0()中,查看readEnum()方法,会发现枚举类型是通过类名和类对象找到一个唯一的枚举对象,枚举对象不可能被类加载器加载多次
		case TC_ENUM:
                    return checkResolve(readEnum(unshared));
  • 能不能通过反射破坏呢?
    答案是不会的
    测试代码:
public static void main(String[] args) throws Exception {
        Class clazz = EnumSingleton.class;
        Constructor constructor =  clazz.getDeclaredConstructor();
        constructor.newInstance();
    }
结果:(没有找到构造函数)

在这里插入图片描述
打开源码就能发现,Java.lang.Enum中只有一个构造方法,没有空参构造器

protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

那么尝试一下通过用这个构造器反射
测试代码:

public static void main(String[] args) throws Exception{
        Class clazz = EnumSingleton.class;
        Constructor constructor = clazz.getDeclaredConstructor(String.class,int.class);
        constructor.setAccessible(true);
        EnumSingleton enumSingleton = (EnumSingleton) constructor.newInstance("Tom",666);
    }

结果跟猜测一样(无法通过反射创建枚举对象)
在这里插入图片描述
原因是什么呢,进入Constructor中的newInstance()方法中,发现如下代码,意思就是
如果修饰符是Modifier.ENUM,直接抛出异常

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
 throw new IllegalArgumentException("Cannot reflectively create enum objects");

5.2 容器式单例

直接上代码

public class ContainerSingleton {
    public static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();
    public static Object getBean(String className){
        synchronized (ioc){
            if (!ioc.containsKey(className)) {
                Object object= null;
                try {
                    object = Class.forName(className).newInstance();
                    ioc.put(className,object);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return object;
            } else {
                return ioc.get(className);
            }
        }
    }
}

容器式单例模式适用于实例非常多的情况,便于管理,属于非线程安全的

总结:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值