单例模式

单例模式

课堂目标:

1.掌握单例模式的应用场景

2.掌握IDEA环境下的多线程调试方式

3.掌握保证线程安全的单例模式策略

4.掌握反射暴力攻击单利解决方案及原理解析

5.序列化破坏单利的原理及解决方案

6.掌握常见的单例模式写法

课前准备

1、列举几个Spring框架中你所熟知的用到单例模式的类。

2、思考:如何才能确保单例对象的线程安全呢?

3、思考:如何才能保证单例不被破坏?

一 、单例模式的应用场景

单例模式(Singleton Pattern )是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。隐藏其所有的构造方法。一般用getInstance()方法名创建单例实例。

单例模式是创建型模式。

应用非常广泛,例如:J2EE中的ServletContext、ServletContextConfig等。Spring中的ApplicationContext、数据库的连接池等都是单例模式。

二、 单例模式的常见写法

饿汉式单例;

懒汉式单例;

注册式单例;

ThreadLocal单例;

2.1 饿汉式单例的2种写法与优缺点

HungrySingleton

public class HungrySingleton {
    private HungrySingleton(){}
    private static final HungrySingleton INSTANCE = new HungrySingleton();

    public static HungrySingleton getInstance(){
        return INSTANCE;
    }
}

HungryStaticSingleton

public class HungryStaticSingleton {
    private HungryStaticSingleton(){}
    private static final HungryStaticSingleton instance;
    static {
        instance = new HungryStaticSingleton();
    }

    public static HungryStaticSingleton getInstance(){
        return instance;
    }
}
/*两种饿汉单例模式写法不同,实际上一模一样,第二种只是用来提升逼格*/

**优点:**执行效率高,没有任何的锁。

**缺点:**在某些情况下会造成内存浪费,项目中需要单例的Bean过多的时候,造成内存浪费。

2.2 懒汉式单例之简单写法与优缺点

LazySimpleSingleton

public class LazySimpleSingleton {
    private LazySimpleSingleton(){}
    private static LazySimpleSingleton instance;
    public static LazySimpleSingleton getInstance(){
        if (instance == null){
            instance = new LazySimpleSingleton();
        }
        return instance;
    }
}

被外部类使用时才创建实例

**优点:**节省内存

**缺点:**非线程安全

验证

// 创建线程类
public class MyThread extends Thread {
    @Override
    public void run() {
        LazySimpleSingleton instance = LazySimpleSingleton.getInstance();
        System.out.println(instance);
    }
}
// 测试类
public class Test {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        thread1.start();
        thread2.start();
        System.out.println("END");
    }
}
// 控制台打印
END
com.gy.lazy.simpleLazy.LazySimpleSingleton@32e3276d
com.gy.lazy.simpleLazy.LazySimpleSingleton@1d296048

**解决办法:**给方法上枷锁

public class LazySimpleSingleton2 {
    private LazySimpleSingleton2(){}
    private static LazySimpleSingleton2 instance;
    public synchronized static LazySimpleSingleton2 getInstance(){
        if (instance == null){
            instance = new LazySimpleSingleton2();
        }
        return instance;
    }
}

**缺点:**如果加锁synchronized,实现线程安全,则会造成性能低。

2.3 懒汉式单例之双重检查锁写法与优缺点

被外部类使用时才创建实例

LazyDoubleCheckSingleton

针对性能低的问题,解决办法,先判断是否存在实例,再给创建对象加锁,再锁里面在判断一次是否存在实例。

public class LazyDoubleCheckSingleton {
    private LazyDoubleCheckSingleton(){}
    private volatile static LazyDoubleCheckSingleton instance;
    public static LazyDoubleCheckSingleton getInstance(){
        // 判断是否加锁
        if(instance == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                // 判断是否新建实例
                if (instance == null) {
                    instance = new LazyDoubleCheckSingleton();
                    // 指令重排序的问题,导致线程紊乱。用volatile关键字解决。
                }
            }
        }
        return instance;
    }
}	

**优点:**提高性能,并且线程安全

**缺点:**程序可读性差

2.4 懒汉式单例之静态内部类写法与优缺点

被外部类使用时才创建实例

LazyStaticInnerClassSingleton,内部类 LazyHolder

public class LazyStaticInnerClassSingleton {
    // 内部类创建实例
    private static class LazyHolder{
        private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
    }
    
    private LazyStaticInnerClassSingleton(){}
    
    public LazyStaticInnerClassSingleton getInstance(){
        return LazyHolder.INSTANCE;
    }
}

**优点:**代码优雅,利用了java本身的语法特点,性能高,避免内存浪费

**缺点:**以上几种单例模式的通病,能够被反射破坏

**解决方法:**通过在构造器中判断内部类不为空则抛出异常,以达到不被反射破坏的目的

三、 反射是如何破坏单例的?

通过反射创建实例:

public class Test {
    public static void main(String[] args) {
        // 拿到反射
        Class<?> clazz = LazyStaticInnerClassSingleton.class;

        try {
            // 获取构造方法
            Constructor<?> c = clazz.getDeclaredConstructor();
            // 设置构造方法访问权限为public
            c.setAccessible(true);
            Object obj1 = c.newInstance();
            Object obj2 = c.newInstance();
            System.out.println(obj1);
            System.out.println(obj2);
            System.out.println(obj1 == obj2);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

    }
}

// 控制台打印
com.gy.lazy.staticInnerClass.LazyStaticInnerClassSingleton@7382f612
com.gy.lazy.staticInnerClass.LazyStaticInnerClassSingleton@1055e4af
false

解决办法:

public class LazyStaticInnerClassSingleton2 {
    // 内部类创建实例
    private static class LazyHolder{
        private static final LazyStaticInnerClassSingleton2 INSTANCE = new LazyStaticInnerClassSingleton2();
    }
    private LazyStaticInnerClassSingleton2(){
        if (LazyHolder.INSTANCE != null){
            throw new RuntimeException("非法访问");
        }
    }

    public LazyStaticInnerClassSingleton2 getInstance(){
        return LazyHolder.INSTANCE;
    }
}

// 控制台打印 抛出异常
java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.gy.lazy.staticInnerClass.Test.main(Test.java:23)
Caused by: java.lang.RuntimeException: 非法访问
	at com.gy.lazy.staticInnerClass.LazyStaticInnerClassSingleton2.<init>(LazyStaticInnerClassSingleton2.java:17)
	... 5 more

四、 注册式单例

**定义:**将每一个实例都缓存到统一的容器中,使用唯一标识符获取单例。

4.1 枚举式单例写法与优缺点

优点:代码优雅,线程安全

缺点:造成内存浪费

等同于饿汉模式,但是不能通过反射破坏单例,因为枚举类不能通过反射创建实例。

EnumSingleton

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;
    }
}

4.2 容器式单例写法与优缺点

ContainerSingleton

public class ContainerSingleton {
    private ContainerSingleton(){}
    // 容器
    private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();
    //  反射创建实例,放入容器中
    public static Object getInstance(String className){
        Object instance = null;
        synchronized (ContainerSingleton.class) {
            if (!ioc.containsKey(className)) {
                try {
                    instance = Class.forName(className).newInstance();
                    ioc.put(className, instance);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return instance;
            } else {
                return ioc.get(className);
            }
        }
    }
}

优点:线程安全,效率快

缺点:序列化会破坏单例

五、 序列化是如何破坏单例的?

public class SeriableSingleton {
    
    public  final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }

}


// 测试类
public class Test {
    public static void main(String[] args) {
//        Student instance1 = (Student)ContainerSingleton.getInstance("com.gy.register.Student");
//        Student instance2 = (Student)ContainerSingleton.getInstance("com.gy.register.Student");
//        System.out.println(instance1 == instance2);

        SeriableSingleton s1 = null;
        SeriableSingleton s2 = SeriableSingleton.getInstance();

        FileOutputStream fos = null;
        try {

            fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            // 序列化结果赋值s1
            s1 = (SeriableSingleton)ois.readObject();
            ois.close();

            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
// 结果
com.gy.register.SeriableSingleton@7cca494b
com.gy.register.SeriableSingleton@7f31245a
false

解决方法:防止序列化破坏单例模式,加入readResolve()方法,方法名与返回值为固定写法。

public class SeriableSingleton implements Serializable {

    public  final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }

    private Object readResolve(){ return INSTANCE;}
}
// 结果
com.gy.register.SeriableSingleton@7f31245a
com.gy.register.SeriableSingleton@7f31245a
true

六、 ThreadLocal单例介绍

ThreadLocalSinglton

public class ThreadLocalSingleton {
    private ThreadLocalSingleton(){}

    private static final ThreadLocal<ThreadLocalSingleton> INSTANCE =
            new ThreadLocal<ThreadLocalSingleton>(){
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };

    public static ThreadLocalSingleton getInstance(){
        return INSTANCE.get();
    }
}

线程类

public class MyThread extends Thread {
    @Override
    public void run() {
        ThreadLocalSingleton instance = ThreadLocalSingleton.getInstance();
        System.out.println(instance);
    }
}

测试类

public class Test{
    public static void main(String[] args) {
        ThreadLocalSingleton t1 = ThreadLocalSingleton.getInstance();
        ThreadLocalSingleton t2 = ThreadLocalSingleton.getInstance();
        System.out.println(t1 == t2);
        System.out.println("--------------------------------------------------");
        MyThread thread1 = new MyThread();
        thread1.start();
        MyThread thread2 = new MyThread();
        thread2.start();
    }


}
// 结果
true
--------------------------------------------------
com.gy.threadLocal.ThreadLocalSingleton@7c2aa267
com.gy.threadLocal.ThreadLocalSingleton@731f7d35

**结论:**不是真正的全局单例,只是保证线程内部的全局唯一,但是天生线程安全。

七、单例模式在源码中的应用

spring中的:AbstractFactoryBean类getObject()方法

mybatis中的:ErrorContext类应用到了ThreadLocal单例

结合源码实例更好理解设计模式。

八、总结

1、优点

在内存中只有一个实例,减少了内存开销;

可以避免对资源的多重占用;

设置全局访问点,严格控制访问。

2、缺点

没有接口,很难扩展;

如果要扩展单例对象,只能修改代码,没有其他途径。

3、知识点

1、私有化构造器;

2、保证线程安全;

3、延迟加载;

4、防止序列化与反序列化;

5、防止反射破坏单例。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值