弄懂软件设计模式(一):单例模式和策略模式

前言

        软件设计模式和设计原则是十分重要的,所有的开发框架和组件几乎都使用到了,比如在这小节中的单例模式就在SpringBean中被使用。在这篇文章中荔枝将会仔细梳理有关单例模式和策略模式的相关知识点,其中比较重要的是掌握单例模式的常规写法。希望对有需要的小伙伴有帮助~~~


文章目录

前言

一、单例模式singleton

1.1 饿汉式

1.2 懒汉式

1.3 懒汉式+悲观锁

1.4 双重检查锁

1.5 静态内部类写法

1.6 枚举单例 

二、策略模型Strategy

总结


一、单例模式singleton

        单例模式确保仅创建一个实例且避免在同一个项目中创建多个实例。其实就是在一次类加载中,只会对当前的类对象创建一次实例,我们不能通过new方法来实例化对象,而是只能调用类对象提供的getInstance方法获取已实例化的对象。

1.1 饿汉式

饿汉式相对来说是使用的比较多的一种单例模式的写法,在类中静态定义一个私有变量并在类加载的时候实例化该类对象。通过在getInstance方法中返回INSTANCE实例对象。

package com.mashibing.dp.singleton;

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

    private Mgr01() {};

    public static Mgr01 getInstance() {
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        Mgr01 m1 = Mgr01.getInstance();
        Mgr01 m2 = Mgr01.getInstance();
        System.out.println(m1 == m2);
    }
}

饿汉式是立即加载的,除了预防反序列化的问题之外几乎没有缺点,而且它是线程安全的,操作简单。

1.2 懒汉式

懒汉式不会在加载类的时候就实例化对象,是懒加载的(按需加载),但是会出现线程安全的问题。 

比如这里两个线程同时打到INSTANCE上,就可能会有同时new出实例对象的风险,因此线程不安全。下面的示例demo中可以看到一个lambda表达式描述的方法。Lambda表达式是对线程Runnable接口匿名内部类的一种简写,这是因为我们这里在Runnable中只写一种内部方法

package com.mashibing.dp.singleton;

public class Mgr03 {
    private static Mgr03 INSTANCE;

    private Mgr03() {
    }
    /**
     * 懒汉式
     */
    public static Mgr03 getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr03();
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->
                System.out.println(Mgr03.getInstance().hashCode())
            ).start();
        }
    }
}

 这里的new Thread()原本写法是

    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println(Mgr03.getInstance().hashCode())
        }
    }).start();

1.3 懒汉式+悲观锁

懒汉式加锁其实比较简单,直接使用synchronized关键字修饰加上悲观锁就可以了,操作比较简单,也比较完好地解决了线程安全问题,但这却是以牺牲效率为前提的,同时也并非序列化安全和反射安全的。

package com.mashibing.dp.singleton;

public class Mgr04 {
    private static Mgr04 INSTANCE;

    private Mgr04() {
    }

    /**
     * 懒汉式+同步锁
     * @return
     */
    public static synchronized Mgr04 getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr04();
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr04.getInstance().hashCode());
            }).start();
        }
    }
}

1.4 双重检查锁

        前面我们为了解决懒汉式带来的线程安全问题加入了锁机制,但却带来了代码效率的下降。这里可以使用双重检查的机制来解决代码效率的问题,优化了代码性能,同时也保证了线程安全和懒加载的机制。但实现起来确实略显复杂,调试也比较困难。

package com.mashibing.dp.singleton;

public class Mgr06 {
    //这里需要加上volatile的原因是因为Java中在编译中指令重排比较频繁,如果不加volatile会出现问题,
    private static volatile Mgr06 INSTANCE; //JIT

    private Mgr06() {
    }
    /**
     *双重检查单例写法
     * @return
     */
    public static Mgr06 getInstance() {
        if (INSTANCE == null) {
            //双重检查
            synchronized (Mgr06.class) {
                if(INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Mgr06();
                }
            }
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr06.getInstance().hashCode());
            }).start();
        }
    }
}

这里需要注意的是在静态变量中INSTANCE需要加上volatile关键字修饰!

volatile关键子的作用

  • 确保INSTANCE变量的可见性,防止出现空指针异常的问题

        被volatile修饰的变量在线程访问时会被强制从主内存中读取变量的值而不从本地缓存中读取,保证共享变量的可见性和有序性,在对该变量进行修改后线程会强制将更新后的值刷回主内存,而不仅仅更新线程的本地缓存。出现空指针异常的问题可能是因为其它线程无立即获取修改后的未被volatile关键字修饰的变量值。

  • 防止指令重排

        指令重排是CPU为了提高程序执行效率而执行的操作,如果INSTANCE变量未被volatile修饰,那么可能无法保证线程安全。

1.5 静态内部类写法

         可以看到静态内部类的写法会在对象类中自定义一个私有的静态内部类,在其中实例化对象并赋值给一个静态常量。既实现了实例化对象的懒加载,同时也保证了线程安全。该类的缺点是对于传参的限制在某些场景下可能不太使用。

package com.mashibing.dp.singleton;

public class Mgr07 {

    private Mgr07() {
    }

    private static class Mgr07Holder {
        private final static Mgr07 INSTANCE = new Mgr07();
    }

    /**
     * 静态内部类的写法
     * @return
     */
    public static Mgr07 getInstance() {
        return Mgr07Holder.INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr07.getInstance().hashCode());
            }).start();
        }
    }


}

1.6 枚举单例 

枚举单例是最完美的单例模式,有效的解决了Java类的反序列化问题,实现了序列安全和反射安全。但枚举单例并不是懒加载的,也不能被继承。

package com.mashibing.dp.singleton;

/**
 * 不仅可以解决线程同步,还可以防止反序列化。
 */
public enum Mgr08 {

    INSTANCE;

    public void m() {}

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr08.INSTANCE.hashCode());
            }).start();
        }
    }

}

非枚举类的单例模式会出现反序列化的问题,这时因为我们可以利用Java的反射机制通过Java中的.class文件加载class对象

枚举单例不能够被反序列化的原因:枚举类没有构造方法。

这里有关双重检查锁参考了掘金大佬的文章,出处如下:

https://juejin.cn/post/7206529406612062268?searchId=20230905212839127143297911190A3F76#heading-16


二、策略模型Strategy

        策略模型比较简单,在日常开发中的使用也比较多,策略模型中一般封装的是实现一个方法的不同执行方式。策略模型将对象和行为分开,属于行为型模式。行为被分为了行为策略接口和实现行为的类。

main文件 

主程序调用比较类Sort,传入类对象和相应的比较器接口的实现即可。 

package com.mashibing.dp.strategy;

import java.util.Arrays;

/**
 * writing tests first!
 * extreme programming
 */
public class Main {
    public static void main(String[] args) {
        Cat[] a = {new Cat(3, 3), new Cat(5, 5), new Cat(1, 1)};
        Sorter<Cat> sorter = new Sorter<>();
//        Dog[] b = {new Dog(3), new Dog(5), new Dog(1)};
//        Sorter<Dog> sorter = new Sorter<>();
       
        /**
         * 策略模式的选择,通过类加载的方式实现功能,代码的拓展性更强
         */
        sorter.sort(a,new CatWeightComparator());
        System.out.println(Arrays.toString(a));
        sorter.sort(a,new CatHeightComparator());
        System.out.println(Arrays.toString(a));
    }
}

Sort类

自定义一个策略选择类,在其中调用已经被重写了的comparator接口中的compare方法实现对传入的比较类的策略模型的调用。 

package com.mashibing.dp.strategy;

public class Sorter<T> {

    public void sort(T[] arr, Comparator<T> comparator) {
        for(int i=0; i<arr.length - 1; i++) {
            int minPos = i;

            for(int j=i+1; j<arr.length; j++) {
                minPos = comparator.compare(arr[j],arr[minPos])==-1 ? j : minPos;
            }
            swap(arr, i, minPos);
        }
    }

}

策略实现类

策略接口需要实现Java.util中的Comparator接口并重写其中的compare方法实现对象类的策略逻辑封装。

package com.mashibing.dp.strategy;

public class CatHeightComparator implements Comparator<Cat> {
    @Override
    public int compare(Cat o1, Cat o2) {
        if(o1.height > o2.height) return -1;
        else if (o1.height < o2.height) return 1;
        else return 0;
    }
}

        其实最简单的策略模型的应用就是通过 if...else... 来判断执行策略,但是这种方式相比而言比较混乱,可拓展性不是很好,因此需要通过接口实现类和Java泛型来自定义一些策略以供选择。


总结

        上面的内容中荔枝主要梳理了单例模式和策略模式,这是二十三种软件设计模式中的两种,理解几种典型的单例模式的写法,接下来的文章中荔枝也会持续学习并整理输出,希望未来越来越好哈哈哈哈哈~~~

今朝已然成为过去,明日依然向往未来!我是小荔枝,在技术成长的路上与你相伴,码文不易,麻烦举起小爪爪点个赞吧哈哈哈~~~ 比心心♥~~~

猜你喜欢

转载自blog.csdn.net/qq_62706049/article/details/132694837