单例模式&多例模式

初识单例模式

    单例模式规定一个类仅有一个实例,并提供一个访问它的全局访问点。就是说单例类必须满足一下几点

  1. 单例类只有一个实例
  2. 单例类必须自己创建这个实例
  3. 必须向整个系统提供这个实例

    单例模式作为众多设计模式中最简单的设计模式之一,原理非常简单,下面主要介绍单例模式的几种不同的实现方式。

实现方式

饿汉式

public class HungarySingleton {

    private HungarySingleton(){};
    // 静态变量
    private static final HungarySingleton instance = new HungarySingleton();

    // 必须使用static,不加就需要调用方创建实例然后调用,而构造函数是私有的
    public static HungarySingleton getInstance(){
        return instance;
    }
}

    之所以成为“饿汉式”是因为在加载类的时候已经完成了静态变量的初始化。这个方式的优点就是不用考虑线程安全问题,缺点也很明显,如果一个对象初始化需要很长的时间而又没有被调用,就造成了资源浪费。

懒汉式

    又称为延迟加载,在需要调用时候才会去创建对象。

public class LazySingleton {

    private LazySingleton() {}
    // 使用静态变量记录类的唯一实例
    private static LazySingleton instance = null;

    public static LazySingleton getInstance(){
        if(instance == null){
            instance = new LazySingleton();
        }
        return instance;
    }
}

    以上代码在单线程环境下没有任何问题,但是在多线程环境下就会出现创建多个实例的问题,解决方法非常简单,使用同步即可,可以使用同步方法或同步代码块,又因为同步比较消耗性能,在判断instance为null时再使用同步代码块,改进后的代码如下

public class LazySingleton {

    private LazySingleton() {}
    // 使用静态变量记录类的唯一实例
    private static LazySingleton instance = null;

    public static LazySingleton getInstance(){
        if(instance == null){                    // 1
            // 同步代码块
            synchronized (LazySingleton.class) { // 2
                instance = new LazySingleton();  // 3
            }
        }
        return instance;
    }
}

    同步是非常浪费性能的,因为一次只能有一个线程执行,其它线程等待,所有在同步之前先判断instance是否为空,然后在同步。分析一下以上代码:有两个线程A和线程B,都是首次执行,假设现在线程A和B都执行到“代码1处”,继续向下执行,假设线程A获得锁执行同步代码块,线程B在“代码2处”等待,线程A执行到“代码3处”创建对象退出同步代码块并释放锁,此时已经创建了对象。然后线程B获得锁,执行“代码3处”再次创建对象,这就导致出现了多个对象。解决方法也很简单,在同步代码块内部做一次非空判断即可,这也是所谓的“双重检测”。

双重检测

public class DoubleSingleton {

    private volatile static DoubleSingleton instance;
    private DoubleSingleton(){}

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

    public static void main(String[] args) {
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10000; i++) {
            pool.submit(new Runnable() {

                @Override
                public void run() {
                    System.out.println(DoubleSingleton.getInstance().hashCode());
                }
            });
        }
        pool.shutdown();
    }
}   

    需要注意instance变量需要使用volatile关键词修饰,来保证在多线程下能够正确的处理instance变量,必须在jdk5之后才可以使用volatile关键字。假如没有volatile关键字修饰的话,会出现一个问题:在Java编译器中,JVM会对代码进行优化,也就是重排序,也就是说DoubleSingleton类的初始化和instance变量赋值顺序 不可预料,假如一个线程在没有同步化的条件下读取instance,并调用该对象的方法,可能对象的初始化还没有完成,从而造成程序错误。
一直没有模拟出来没有volatile关键字修饰程序会出现什么错误,希望看到的小伙伴不吝赐教!!


    可以看到使用饿汉式实现单例模式优点就是不用考虑多线程,缺点是比较占用内存。懒汉式方式比较繁琐,volatile也会降低程序的性能。那么有没有一种结合两者优点的方式呢?可以使用静态内部类来完成

静态内部类

public class InnerSingleton {

    private InnerSingleton(){}
    private static class Singleton{
        private static  InnerSingleton single = new InnerSingleton();
    }

    public static InnerSingleton getInstance(){
        return Singleton.single;
    }
}

    只有在调用getInstance()方法时才会创建对象,既保证了线程安全,又保证了延迟加载。

枚举方式

    《Effective Java》第三条:Java5之后,可以使用枚举实现单例,可以防止多次序列化以及反射攻击,同时又非常简洁,可以说是单例模式最佳的实现方式。

public enum EnumSingleton {

    INSTANCE;

    public void print(){
        System.out.println("I am EnumSingleton");
    }
}

实现多例模式

    下面对单例模式进行改造,使用缓存的思想模拟实现“多例模式”,既程序中一个类的实例对象可以存在有限多个(比如说3个),和单例模式唯一的区别就是程序中可以有多个实例对象

public class SomeSingleton {

    private SomeSingleton() {}
    // 存储创建的实例(模拟缓存)
    private static Map map = new HashMap<>(3);
    // 计数器从1开始
    private volatile static int num = 1;
    // 控制最多有三个实例
    private static final int MAX_NUM = 3;
    // 锁
    final static ReentrantLock lock = new ReentrantLock();

    public static SomeSingleton getInstance() {
        lock.lock();
        try {
            // 从缓存中取出实例
            SomeSingleton instance  = map.get(num);
            if (instance == null) {
                instance = new SomeSingleton();
                // 计数器作为map的key
                map.put(num, instance);
            }
            // 计数器+1
            num++;
            if (num > MAX_NUM) {
                // 重置计数器
                num = 1;
            }
            return instance;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        final Set set = new HashSet<>();
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10000; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    // System.out.println(getInstance().hashCode());
                    set.add(getInstance().hashCode());
                }
            });
        }
        System.out.println(set.size());
        pool.shutdown();
    }
}

    不管有多少个线程调用,都只会创建3个实例对象,完成了控制有限过个实例对象。

小结

    单例模式的实质就是控制实例对象在程序中的数量有且仅有一个,并且只能自己创建。实现单例可以使用“静态内部类”和“枚举”方式,不过“静态内部类”需要注意反射攻击以及序列化破坏。

JDK中的单例模式
    java.lang.Runtime
    java.text.NumberFormat
Spring中
    在Spring中默认的bean都是单例的

你可能感兴趣的:(设计模式)