设计模式汇总-单例模式实现方式

单例模式实现方式汇总

饿汉式单例

1、饿汉式单例-<静态成员属性>[可用]

/**
 * 饿汉式单例
 * 该模式的特点是类一旦加载就创建一个单例,保证在调用 getInstance 方法之前单例已经存在了。
 * 饿汉式单例在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的,
 * 可以直接用于多线程而不会出现线程安全问题。
 * <守护线程或初始化资源线程 可以使用>
 */
public class HungrySingleton {
    private static final HungrySingleton instance = new HungrySingleton(); // 1.
private HungrySingleton() {} // 2.
public static HungrySingleton getInstance() { //3.
        return instance;
    }
}

优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。

缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。

2、饿汉式单例-<静态代码块>[可用]

/**
 * 饿汉式的拓展写法
 */
public class HungrySingleton2 {
    private static HungrySingleton2 instance = null;
​
    static {
        instance = new HungrySingleton2();
    }
​
    private HungrySingleton2() {}
​
    public static HungrySingleton2 getInstance() {
        return instance;
    }
}

 这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。

注册式单例

/**
 * 类似Spring里面的方法,将类名注册,下次从里面直接获取
 */
public class RegisteredSingleton {
    // 存储需要进行维护和管理的类的实例
    private static Map<String, RegisteredSingleton> map = new HashMap<String, RegisteredSingleton>();
    static {
        // 静态创建实例并添加到Map集合
        RegisteredSingleton singleton = new RegisteredSingleton();
        map.put(singleton.getClass().getSimpleName(), singleton);
    }
​
    // 保护的默认构造子
    protected RegisteredSingleton() {
    }
​
    // 静态工厂方法,返还此类惟一的实例
    public static RegisteredSingleton getInstance(String name) {
        if (name == null) {
            name = RegisteredSingleton.class.getName();
            System.out.println("name == null" + "--->name=" + name);
        }
        if (map.get(name) == null) {
            try {
                map.put(name, (RegisteredSingleton) Class.forName(name).newInstance());
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        return map.get(name);
    }
​
    public static void main(String[] args) {
        RegisteredSingleton singleton = RegisteredSingleton.getInstance("RegisteredSingleton");
        System.out.println(singleton);
    }
}

 

枚举单例 [推荐用]

单元素的枚举类型已经成为实现Singleton的最佳方法
                      -- 出自 《effective java》

public enum  EnumSingleton {
    INSTANCE;
    public EnumSingleton getInstance(){
        return INSTANCE;
    }
}

本质上也是注册式单例 

借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化、反射 等重新创建新的对象。可能是因为枚举在JDK1.5中才添加,所以在实际项目开发中,很少见人这么写过。但却是保证单例最保险的一种方式。

懒汉式单例

一般写法 [不可用]

public class LazySingleton {
    //懒汉式单例模式
    //比较懒,在类加载时,不创建实例,因此类加载速度快,但首次运行时获取对象的速度慢
private static LazySingleton intance = null;//静态私用成员,没有初始化
private LazySingleton() {
        //私有构造函数
    }
​
    //静态,公开访问点
    public static LazySingleton getInstance() {
        if (intance == null) {
            intance = new LazySingleton();
        }
        return intance;
    }
}

 

这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式,

由于这种写法在多线程环境中是存在线程安全问题的

接下来才有了对此不断改进的写法:

同步锁 [不推荐用]

/**
 * 懒汉式单例
 * 该模式的特点是类加载时没有生成单例,只有当第一次调用 getlnstance 方法时才去创建这个单例
 * 如下需要注意三点
 * 如果编写的是多线程程序,则不要删除上例代码中的关键字 volatile 和 synchronized,否则将存在线程不安全的问题
 * 如果不删除这两个关键字就能保证线程安全,但是每次访问时都要同步,会影响性能,且消耗更多的资源,这是懒汉式单例的缺点。
 * <使用次数少可以采用>
 */
public class LazySingleton {
    private static volatile LazySingleton instance = null;    //1. 保证 instance 在所有线程中同步
private LazySingleton() {}    // 2. private 避免类在外部被实例化
public static synchronized LazySingleton getInstance() {
        // 3.getInstance 方法前加同步
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

 解决了上面实现方式的线程不安全问题,做个线程同步就可以了,于是就对getInstance()方法进行了线程同步。

缺点:效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只在首次执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低要改进。

同步代码块 [不推荐用]

/**
 * 懒汉式单例
 * 该模式的特点是类加载时没有生成单例,只有当第一次调用 getlnstance 方法时才去创建这个单例
 * 如下需要注意三点
 * 如果编写的是多线程程序,则不要删除上例代码中的关键字 volatile 和 synchronized,否则将存在线程不安全的问题
 * 如果不删除这两个关键字就能保证线程安全,但是每次访问时都要同步,会影响性能,且消耗更多的资源,这是懒汉式单例的缺点。
 * <使用次数少可以采用>
 */
public class LazySingleton {
    private static volatile LazySingleton instance = null;    //1. 保证 instance 在所有线程中同步
private LazySingleton() {}    // 2. private 避免类在外部被实例化
public static LazySingleton getInstance() {
        // 3.getInstance 方法中创建时代码 加同步
        if (instance == null) {
            synchronized(LazySingleton.class){ 
                 instance = new LazySingleton();
            }
        }
        return instance;
    }
}            

由于上一个实现方式同步效率太低,所以摒弃同步方法,改为同步产生实例化的代码块。但是这种同步并不能起到线程同步的作用。跟上一个实现方式遇到的情形一致,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。

双重校验锁的单例 [推荐用]

/**
 * 双重锁校验的单例
 */
public class DoubleLock implements Serializable {
​
    public static volatile DoubleLock doubleLock = null;//volatile防止指令重排序,内存可见(缓存中的变化及时刷到主存,并且其他的内存失效,必须从主存获取)
private DoubleLock() {
        //构造器必须私有  不然直接new就可以创建
    }
​
    public static DoubleLock getInstance() {
        //第一次判断,假设会有好多线程,如果doubleLock没有被实例化,那么就会到下一步获取锁,只有一个能获取到,
        //如果已经实例化,那么直接返回了,减少除了初始化时之外的所有锁获取等待过程
        if (doubleLock == null) {
            synchronized (DoubleLock.class) {
                //第二次判断是因为假设有两个线程A、B,两个同时通过了第一个if,然后A获取了锁,进入然后判断doubleLock是null,他就实例化了doubleLock,然后他出了锁,
                //这时候线程B经过等待A释放的锁,B获取锁了,如果没有第二个判断,那么他还是会去new DoubleLock(),再创建一个实例,所以为了防止这种情况,需要第二次判断
                if (doubleLock == null) {
                    //下面这句代码其实分为三步:
                    //1.开辟内存分配给这个对象
                    //2.初始化对象
                    //3.将内存地址赋给虚拟机栈内存中的doubleLock变量
                    //注意上面这三步,第2步和第3步的顺序是随机的,这是计算机指令重排序的问题
                    //假设有两个线程,其中一个线程执行下面这行代码,如果第三步先执行了,就会把没有初始化的内存赋值给doubleLock
                    //然后恰好这时候有另一个线程执行了第一个判断if(doubleLock == null),然后就会发现doubleLock指向了一个内存地址
                    //这另一个线程就直接返回了这个没有初始化的内存,所以要防止第2步和第3步重排序
                    // 采用volatile修饰单例引用
                    doubleLock = new DoubleLock();
                }
            }
        }
        return doubleLock;
    }
}

 Double-Check-Lock 概念对于多线程开发者来说不会陌生,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象。

优点:线程安全;延迟加载;效率较高。

其实上面的这个写法经历了一个 没有写volatile修饰单例引用的中间过程,后来再次优化才变成这个写法

静态内部类 [推荐用]

/**
 * 线程安全的懒汉式单例
 * 用静态内部类实现单例模式
 *
 * 1.从外部无法访问静态内部类LazyHolder,只有当调用Singleton.getInstance方法的时候,
 * 才能得到单例对象INSTANCE。
 *
 * 2.INSTANCE对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,
 * 使得静态内部类LazyHolder被加载的时候。因此这种实现方式是利用classloader
 * 的加载机制来实现懒加载,并保证构建单例的线程安全。
 *
 * 真正的懒加载方式
 */
public class InnerClassSingleton {
    // 私有内部类,按需加载,用时加载,也就是延迟加载
    private static class Holder {
        // jvm保证在任何线程访问singleton5静态变量之前一定先创建了此实例
        private static final InnerClassSingleton singleton5 = new InnerClassSingleton();
    }
​
    private InnerClassSingleton() {
    }
​
    public static InnerClassSingleton getSingleton5() {
        return Holder.singleton5;
    }
}

 这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化

类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

优点:避免了线程不安全,延迟加载,效率高。

ThreadLocal -[推荐]

/**
 * 线程安全的懒汉式单例
 */
public class ThreadLocalSingleton {
    // ThreadLocal 线程局部变量
    private static ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal<ThreadLocalSingleton>();
    private static ThreadLocalSingleton singleton4 = null;
​
    private ThreadLocalSingleton() {}
​
    public static ThreadLocalSingleton getSingleton4() {
        if (threadLocal.get() == null) {        // 第一次检查:该线程是否第一次访问
            createSingleton4();
        }
        return singleton4;
    }
​
    public static void createSingleton4() {
        synchronized (ThreadLocalSingleton.class) {
            if (singleton4 == null) {          // 第二次检查:该单例是否被创建
                singleton4 = new ThreadLocalSingleton();   // 只执行一次
            }
        }
        threadLocal.set(singleton4);      // 将单例放入当前线程的局部变量中
​
    }
}

 如果需要满足线程内实现 单例的场景 推荐这种方式 (也是属于注册式单例)

一般场景下 通过单例模式的方法创建的类在当前进程中只有一个实例,但有时根据需要,也有可能在一个线程中属于单例,如:仅线程上下文内使用同一个实例。


总结

     优缺点: 

        优点

        系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。

       缺点

        当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new ,可能会给其他开发人员造成理解误会(如果命名不合理)

     适用场合

  • 需要频繁的进行创建和销毁的对象;
  • 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
  • 工具类对象;
  • 频繁访问数据库或文件的对象。
不适用于变化频繁的对象 !!!

目前所有实现单例主流方式的汇总就告一段了。
下一篇讲一讲 ,单例模式的安全攻击问题
(如何防止 反射 序列化 克隆 类加载器 导致 单例模式结构约束的破坏问题)

 

 

 

 

 

posted @ 2020-09-11 11:55  程序员蜗牛  阅读(178)  评论(1)    收藏  举报