单例模式(学习笔记)

单例模式

可以说是最简单的设计模式

当前进程确保一个类全局只有一个实例

优点:

  • 单例模式在内存中只有一个实例,减少了内存开支
  • 单例模式只生成一个实例,所以减少了系统的性能开销
  • 单例模式可以避免对资源的多重占用
  • 单例模式可以在系统设置全局的访问点

缺点:

  • 单例模式一般没有接口,扩展很困难
  • 单例模式不利于测试
  • 单例模式与单一职责原则有冲突

什么情况下要用单例模式呢?

  • 要求生成唯一序列号的环境
  • 在整个项目中需要一个共享访问点或共享数据
  • 创建一个对象需要消耗的资源过多
  • 需要定义大量的静态常量和静态方法(如工具类)的环境

饿汉式(线程安全)

就是一上来就将对象加载进内存

package single;

public class Single {
    private  static Single single = new Single();
    private Single(){
        
    }
    private static Single getInstance(){
        return single;
    }
    
}

如因此,不管我们的程序会不会用到,它都会在程序启动之初进行初始化。如果在程序中有大量的内存需要加载在1启动之初却用不到,这就会造成资源的浪费,所以就出现了懒汉式,用不到就不去加载。

懒汉式(线程不安全)

只有用到的时候才会加载,这就是懒加载。

package single;

public class LazyMan {

    private static LazyMan lazyMan;

    private static LazyMan getInstance(){
        if (lazyMan == null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
}

这种模式在单线程下是OK的,但是在并发多线程下就会出现问题。

package single;

public class LazyMan {

    private static LazyMan lazyMan;
    private LazyMan(){
        System.out.println(Thread.currentThread().getName() + "ok");
    }

    private static LazyMan getInstance(){
        if (lazyMan == null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }

    public static void main(String[] args){
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                LazyMan.getInstance();
            }).start();
        }
    }
}

结果理论上会生成个十个线程,打印出十个线程名,可是实际上只会出现一个或几个,会出现一些小问题。

DCL懒汉式

所以就有了双重效验锁模式,也叫DCL懒汉式

package single;
//懒汉式单例
public class LazyMan {

    private static LazyMan lazyMan;
    private LazyMan(){
        System.out.println(Thread.currentThread().getName() + "ok");
    }
//双重检测锁模式 DCL懒汉式
    private static LazyMan getInstance(){
        if (lazyMan == null) {
            synchronized (LazyMan.class){
                if (lazyMan == null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args){
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                LazyMan.getInstance();
            }).start();
        }
    }
}

为什么要双重校验?

如果两个线程一起调用getInstance方法,并且都通过了第一次的判断lazyMan==null,那么第一个线程获取了锁,然后实例化了lazyMan,然后释放了锁,然后第二个线程得到了线程,然后马上也实例化了lazyMan。这就不符合我们的单例要求了。

但是,这里也存在一些问题,在if语句中 lazyMan = new LazyMan();并不是一个原子性操作,他会在底层(或者说内部)执行三个操作

  1. 给 lazyMan分配内存空间
  2. 执行 lazyMan 的构造方法初始化对象
  3. 把这个对象指向这个空间

这就有可能会造成指令重排的现象,也就是说,本来应该安装123的顺序去执行,但是他却是按照132或其他的顺序执行了,如果这是又有一个线程进来而lazyMan还没有分配完内存(或者是没有完成构造),这时就会引发问题,所以要在private volatile static LazyMan lazyMan;这里加上volatile用来防止指令重排这就成了完整的双重校验锁模式了

静态内部类

package single;
//静态内部类
public class Singles {
    private Singles(){

    }
    private static Singles getInstance(){
        return Inner.singles;
    }
    private static class Inner{
        private final static Singles singles = new Singles();
    }
}

静态内部类,不仅能实现懒加载而且线程安全还保持了指令优化的能力。

Singles类被装载时并不会立即实例化,而是在需要实例化的时候调用getInstance()方法,才会加载静态内部类Inner类,从而完成Singles的实例化。

使用反射破坏单例

但是

这也不是绝对的安全的,因为java中有个词叫:反射。

 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        LazyMan instance = LazyMan.getInstance();
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        LazyMan lazyMan = declaredConstructor.newInstance();
        System.out.println(lazyMan);
        System.out.println(instance);
    }

输出结果:

mainok
mainok
single.LazyMan@677327b6
single.LazyMan@14ae5a5

可以清楚的看到这里两个对象是不一样的

那怎样防止反射呢?

我们可以在构造函数中在加一把锁

private LazyMan(){
       synchronized (LazyMan.class){
           if (lazyMan != null){
          throw new RuntimeException("不要使用反射来搞破坏!");
       }
       }
    }

这就有了三把锁

可是如果都用反射来创建对象会怎么样呢?

那就会发现又正常建立对象了

single.LazyMan@677327b6
single.LazyMan@14ae5a5

那怎样解决呢?

这就可以使用一个变量来进行加密

package single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class LazyMan {

    private volatile static LazyMan lazyMan;
    private static boolean flag = false;
    private LazyMan(){
       synchronized (LazyMan.class) {
           if (flag == false){//当第二次创建对象的时候flag==true了就会执行else
               flag = true;
           }else {
               throw new RuntimeException("不要使用反射来搞破坏!");
           }
       }
    }

    private static LazyMan getInstance(){
        if (lazyMan == null) {
            synchronized (LazyMan.class){
                if (lazyMan == null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
       // LazyMan instance = LazyMan.getInstance();
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        LazyMan lazyMan = declaredConstructor.newInstance();
        LazyMan instance = declaredConstructor.newInstance();

        System.out.println(lazyMan);
        System.out.println(instance);
    }
}

现在假设我们知道了你加密的变量名字

我们来破坏他的权限

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
       // LazyMan instance = LazyMan.getInstance();
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        LazyMan lazyMan = declaredConstructor.newInstance();
        //假设我们知道他的加密变量名
        Field flag = LazyMan.class.getDeclaredField("flag");
        flag.setAccessible(true);//解开私有权限
        flag.set(lazyMan,false);//更改变量值
        LazyMan instance = declaredConstructor.newInstance();

        System.out.println(lazyMan);
        System.out.println(instance);
    }

所以可以得出一个结论:道高一尺魔高一丈!

所以,现在我们看一下反射的原码,到底是什么。

newInstance的原码

   @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

到这里我们发现,if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException(“Cannot reflectively create enum objects”);

是枚举类型就不会被反射,所以我们要看一下枚举到底长什么样可以不被反射,直接阅读原码,新建一个枚举类

package single;

public enum SingleEnum {
    instance;
    public SingleEnum getInstance(){
        return instance;
    }
}

然后在idea里找到这个枚举类的.class文件,可以看到他是有空参构造的,这是idea给的原码

package single;

public enum SingleEnum {
    instance;

    private SingleEnum() { /* compiled code */ }

    public single.SingleEnum getInstance() { /* compiled code */ }
}

现在我们使用反射来试试

package single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class EnumTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        SingleEnum singleEnum = SingleEnum.instance;
        Constructor<SingleEnum> constructor = SingleEnum.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        SingleEnum enumTest = constructor.newInstance();

        System.out.println(singleEnum);
        System.out.println(enumTest);
    }
}

在这里插入图片描述

然后你会发现我们被idea给骗了

这个错是说没有方法错误。

那idea骗了我们,我们就用命令行的方式进行查看

D:\idea-1\student\target\classes\single>javap -p Single.class
Compiled from “Single.java”
public class single.Single {
private static single.Single single;
private single.Single();
private static single.Single getInstance();
static {};
}

D:\idea-1\student\target\classes\single>

可以看出

这个和idea给出的是差不多的,cmd也骗了我们。

现在我们使用专业工具jad将.class文件反编译成.java文件,使用命令jad -sjava SingleEnum.class

SingleEnum.java文件如下


// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   SingleEnum.java

package single;


public final class SingleEnum extends Enum
{

    public static SingleEnum[] values()
    {
        return (SingleEnum[])$VALUES.clone();
    }

    public static SingleEnum valueOf(String name)
    {
        return (SingleEnum)Enum.valueOf(single/SingleEnum, name);
    }

    private SingleEnum(String s, int i)
    {
        super(s, i);
    }

    public SingleEnum getInstance()
    {
        return instance;
    }

    public static final SingleEnum instance;
    private static final SingleEnum $VALUES[];

    static 
    {
        instance = new SingleEnum("instance", 0);
        $VALUES = (new SingleEnum[] {
            instance
        });
    }
}

现在修改代码

package single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class EnumTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        SingleEnum singleEnum = SingleEnum.instance;
        Constructor<SingleEnum> constructor = SingleEnum.class.getDeclaredConstructor(String.class,int.class);
        constructor.setAccessible(true);
        SingleEnum enumTest = constructor.newInstance();

        System.out.println(singleEnum);
        System.out.println(enumTest);
    }
}

结果如下,发现这才是我们想要的错误
在这里插入图片描述
本文只是学习时笔记,如有侵权请联系我删除

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值