02.【设计模式】单例模式

1.定义

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

2.适用场景

确保任何情况下都绝对只有一个实例

例如:ServletContext、ServletConfig、ApplicationContext、DBPool

3.单例模式的常见写法

3.1 饿汉式单例(在单例类首次加载时就创建实例)

优点

执行效率高,性能高,没有任何的锁

缺点

类加载的时候就初始化,在某些情况下可能会造成内存浪费,因为不管用不用就会被初始化,内存没浪费的是当使用的时候才去初始化,所以说当大量使用的时候,用饿汉式单例会内存浪费

案例代码

/**
 * 优点:执行效率高,性能高,没有任何的锁
 * 缺点:类加载的时候就初始化,在某些情况下可能会造成内存浪费,
 *      因为不管用不用就会被初始化,内存没浪费的是当使用的时候才去初始化
 *      所以说当大量使用的时候,用饿汉式单例会内存浪费
 */
public class HungrySingleton {
    // static 在类加载的时候就被初始化了
    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton(){

    }

    public HungrySingleton getInstance(){
        return hungrySingleton;
    }

}
  • 静态
public class HungryStaticSingleton {
    //类加载顺序
    //先静态后动态
    //先上,后下
    //先属性后方法
    private static final HungryStaticSingleton hungryStaticSingleton;

    //没什么区别,装B
    static {
        hungryStaticSingleton = new HungryStaticSingleton();
    }

    private HungryStaticSingleton(){

    }

    public HungryStaticSingleton getInstance(){
        return hungryStaticSingleton;
    }

}

3.2 懒汉式单例(被外部调用时才创建实例)

优点

优点:节省了内存

缺点

缺点:出现线程不安全,可能出现两个线程去调用这个实例

案例

/**
 * 优点:节省了内存
 * 缺点:出现线程不安全,可能出现两个线程去调用这个实例
 */
public class LazySimpleSingleton {

    private static LazySimpleSingleton lazySimpleSingleton ;

    // 私有化构造方法
    private LazySimpleSingleton(){

    }

    // 提供一个全局访问点
    public synchronized static LazySimpleSingleton getInstance(){
        if (lazySimpleSingleton == null){
            lazySimpleSingleton = new LazySimpleSingleton();
        }
        return lazySimpleSingleton;
    }
}

验证线程不安全

  • 一个线程
/**
 * 一个线程
 */
public class ExectorThread implements Runnable{
    @Override
    public void run() {
        LazySimpleSingleton lazySimpleSingleton = LazySimpleSingleton.getInstance();
        System.out.println(Thread.currentThread().getName() + ":" +lazySimpleSingleton);
    }
}

  • 测试
public class LazySimpleSingletonTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());
        t1.start();
        t2.start();
        System.out.println("End");
    }
}
运行结果

在这里插入图片描述

  • 同一个实例
    1.正常顺序执行
    2.后者覆盖前者情况
  • 不同的实例
    同时进入条件,按顺序返回

解决线程不安全——加锁(让一个线程执行完再执行另一个线程)

/**
 * 优点:节省了内存
 * 缺点:出现线程不安全,可能出现两个线程去调用这个实例
 *
 * 解决线程不安全——> synchronized加锁
 * 			  ——>新的缺点——瓶颈,性能受到影响
 */
public class LazySimpleSingleton {

    private static LazySimpleSingleton lazySimpleSingleton ;

    // 私有化构造方法
    private LazySimpleSingleton(){

    }

    // 提供一个全局访问点
    // 加锁解决线程不安全
    public synchronized static LazySimpleSingleton getInstance(){
        if (lazySimpleSingleton == null){
            lazySimpleSingleton = new LazySimpleSingleton();
        }
        return lazySimpleSingleton;
    }
}

加锁的影响——新的缺点——瓶颈,性能受到影响

例如高铁站门口检查
在这里插入图片描述

双重检查锁解决加锁的缺点

/**
 * 双重锁
 * 优点:性能高了,线程安全了
 * 缺点:可读性难度加大,不够优雅
 */
public class LazyDoubleCheckSingleton {

    // 双重检查锁会出现指令重排序的问题,要解决的话加上volatile
    private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton;

    private LazyDoubleCheckSingleton(){

    }

    public static LazyDoubleCheckSingleton getInstance(){
        // 检查是否要阻塞
        if (lazyDoubleCheckSingleton == null){
            synchronized (LazyDoubleCheckSingleton.class){
                // 检查是否要重新创建实例
                if (lazyDoubleCheckSingleton == null){
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        
        return lazyDoubleCheckSingleton;
    }
}

解决双重检查锁的不够优雅的缺点(静态内部类写法)

/**
 * 看似饿汉式,实际懒汉式
 * 加载——> LazyStaticInnerClassSingleton.class 一开始加载
 *        LazyStaticInnerClassSingleton$LazyHolder.class 只有到return LazyHolder.INSTANCE;才被加载
 *
 *  优点:写法优雅,利用Java本身语法特点,性能高,避免了内存浪费
 *  缺点:能够被反射破坏1
 */
public class LazyStaticInnerClassSingleton {
    
    private LazyStaticInnerClassSingleton(){
        
    }
    
    private static LazyStaticInnerClassSingleton getInstance(){
        return LazyHolder.INSTANCE;
    }

    /**
     * 静态内部类
     */
    private static class LazyHolder{
        private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
    }
}

还原反射破坏单例的场景

/**
 * 破坏了单例
 */
public class ReflectTest {
    public static void main(String[] args) {
        try {
            Class<?> clazz = LazyStaticInnerClassSingleton.class;
            Constructor<?> c = clazz.getDeclaredConstructor(null);

            // 因为构造方法是私有的,这里设置可以忽视私有
            c.setAccessible(true);

            Object instance1 = c.newInstance();
            Object instance2 = c.newInstance();

            System.out.println(instance1);
            System.out.println(instance2);

            System.out.println(instance1 == instance2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

解决反射破坏单例的情景
/**
 * 看似饿汉式,实际懒汉式
 * 加载——> LazyStaticInnerClassSingleton.class 一开始加载
 *        LazyStaticInnerClassSingleton$LazyHolder.class 只有到return LazyHolder.INSTANCE;才被加载
 *
 *  优点:写法优雅,利用Java本身语法特点,性能高,避免了内存浪费,不能够被反射破坏
 *  缺点:构造方法加上了就不够优雅
 */
public class LazyStaticInnerClassSingleton {

    private LazyStaticInnerClassSingleton(){
        if (LazyHolder.INSTANCE != null){
            throw new RuntimeException("非法访问");
        }
    }

    private static LazyStaticInnerClassSingleton getInstance(){
        return LazyHolder.INSTANCE;
    }

    /**
     * 静态内部类
     */
    private static class LazyHolder{
        private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
    }
}

3.3 注册式单例(将每一个实例都缓存到统一的容器中,使用唯一标识符获取实例)

枚举式案例代码

/**
 * 非常优雅了
 * 但是因为INSTANCE是一开始就声明的,就类似于饿汉式
 * 不适于大批量的情况下使用
 * 接下来就看SpringIOC的了
 */
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 class EnumSingletonTest {
    public static void main(String[] args) {
//        EnumSingleton enumSingleton = EnumSingleton.getInstance();
//        enumSingleton.setData(new Object());

        try {
            Class clazz = EnumSingleton.class;
            Constructor c = clazz.getDeclaredConstructor(String.class, int.class);

            // 因为private所以要setAccessible
            c.setAccessible(true);
            Object o = c.newInstance();
            System.out.println(o);
        }catch (Exception e){
            e.printStackTrace();
        }

    }
}

结果
在这里插入图片描述

容器式解决大规模生产单例的问题

因为枚举不适于大批量的情况下使用,所以可用容器式解决大规模生产单例,用Spring

public class ContainerSingleton {

    private ContainerSingleton(){

    }

    private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();

    public static Object getInstance(String className){
        Object instance = null;
        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 ContainerSingletonTest {
    public static void main(String[] args) {
        Object instance1 = ContainerSingleton.getInstance("com.DesignLearn.SingletonPattern.test.pojo");
        Object instance2 = ContainerSingleton.getInstance("com.DesignLearn.SingletonPattern.test.pojo");
        System.out.println(instance1);
        System.out.println(instance2);
        System.out.println(instance1 == instance2);
    }
}

还原反序列化破坏单例的场景

  • 序列化
    把内存中对象的状态转换为字节码的形式
    把字节码通过IO输出流,写到磁盘上
    永久保存下来,持久化

  • 反序列化
    把持久化的字节码内容,通过IO输入流读到内存中
    转化为一个Java对象

  • 案例代码

public class SeriableSingleton implements Serializable {


    // 序列化
    // 把内存中对象的状态转换为字节码的形式
    // 把字节码通过IO输出流,写到磁盘上
    // 永久保存下来,持久化

    // 反序列化
    // 把持久化的字节码内容,通过IO输入流读到内存中
    // 转化为一个Java对象


    private static  SeriableSingleton seriableSingleton = new SeriableSingleton();

    private SeriableSingleton(){

    }

    public static SeriableSingleton getInstance(){
        return seriableSingleton;
    }
}

  • 测试
public class SeriableSingletonTest {
    public static void main(String[] args) {
        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 =(SeriableSingleton) ois.readObject();
            ois.close();

            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

  • 结果
    在这里插入图片描述
    结果应该要一样的

  • 解决方法

public class SeriableSingleton implements Serializable {


    // 序列化
    // 把内存中对象的状态转换为字节码的形式
    // 把字节码通过IO输出流,写到磁盘上
    // 永久保存下来,持久化

    // 反序列化
    // 把持久化的字节码内容,通过IO输入流读到内存中
    // 转化为一个Java对象


    private static  SeriableSingleton seriableSingleton = new SeriableSingleton();

    private SeriableSingleton(){

    }

    public static SeriableSingleton getInstance(){
        return seriableSingleton;
    }

    // 解决放序列化问题
    private Object readResolve(){
        return seriableSingleton;
    }
}

3.4 ThreadLocal单例(保证线程内部的全局唯一,且天生线程安全)

案例代码

  • ThreadLocalSingleton
public class ThreadLocalSingleton {

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

    private ThreadLocalSingleton(){

    }

    public static ThreadLocalSingleton getInstance(){
        return threadLocalInstance.get();
    }
}
  • 测试ThreadLocalSingletonTest
public class ThreadLocalSingletonTest {

    public static void main(String[] args) {
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());

        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());
        t1.start();
        t2.start();
        System.out.println("End");
    }
}
  • 测试ExectorThread
/**
 * 一个线程
 */
public class ExectorThread implements Runnable{
    @Override
    public void run() {
        ThreadLocalSingleton threadLocalSingleton = ThreadLocalSingleton.getInstance();
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getName() + ":" +threadLocalSingleton);
    }
}

4.总结

单例模式的优点

  • 在内存中只有一个实例
  • 可以避免对资源的多重利用
  • 设置全局访问点,严格控制访问

单例模式的缺点

  • 没有接口,扩展困难
  • 如果要扩展单例对象,只有修改代码,没有其他途径

重点总结

  • 私有化构造器
  • 保证线程安全
  • 延迟加载
  • 防止序列化和反序列化破坏单例
  • 防御反射攻击单例
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
项目:使用 JavaScript 编写的杀死幽灵游戏(附源代码) 杀死鬼魂游戏是使用 Vanilla JavaScript、CSS 和 HTML 画布开发的简单项目。这款游戏很有趣。玩家必须触摸/杀死游荡的鬼魂才能得分。您必须将鼠标悬停在鬼魂上 - 尽量得分。鬼魂在眨眼间不断从一个地方移动到另一个地方。您必须在 1 分钟内尽可能多地杀死鬼魂。 游戏制作 这个游戏项目只是用 HTML 画布、CSS 和 JavaScript 编写的。说到这个游戏的特点,用户必须触摸/杀死游荡的幽灵才能得分。游戏会根据你杀死的幽灵数量来记录你的总分。你必须将鼠标悬停在幽灵上——尽量得分。你必须在 1 分钟内尽可能多地杀死幽灵。游戏还会显示最高排名分数,如果你成功击败它,该分数会在游戏结束屏幕上更新。 该游戏包含大量的 javascript 以确保游戏正常运行。 如何运行该项目? 要运行此游戏,您不需要任何类型的本地服务器,但需要浏览器。我们建议您使用现代浏览器,如 Google Chrome 和 Mozilla Firefox。要玩游戏,首先,单击 index.html 文件在浏览器中打开游戏。 演示: 该项目为国外大神项目,可以作为毕业设计的项目,也可以作为大作业项目,不用担心代码重复,设计重复等,如果需要对项目进行修改,需要具备一定基础知识。 注意:如果装有360等杀毒软件,可能会出现误报的情况,源码本身并无病毒,使用源码时可以关闭360,或者添加信任。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值