设计模式之单例模式

设计模式之单例模式

单例模式(singleton)是设计模式中最简单的模式之一,它作用于单个类,且该类只有一个实例对象。

单例模式的实现有很多种:懒汉,饿汉,加锁,双重加锁等,接下来我将一一说明每种方式的实现。

饿汉模式:

 

/**
 * 饿汉式:不管你是否需要我这个实例,在我类加载的时候都给你初始化出来。
 * 类加载到内存后,就实例化一个单例,JVM保证线程安全
 * 简单实用,推荐使用!
 * 唯一缺点:不管用到与否,类装载时就完成实例化
 */
public class Mgr01 {
    private static final Mgr01 INSTANCE = new Mgr01();
//将构造方法私有化,其他类无法调用该类的构造方法无法对其进行初始化。
private Mgr01() {} public static Mgr01 getInstance() { return INSTANCE; } //进行验证,判断m1,m2是否为同一个对象 public static void main(String[] args) { Mgr01 m1 = Mgr01.getInstance(); Mgr01 m2 = Mgr01.getInstance(); System.out.println(m1 == m2); } }

 


 

懒汉模式

/**
 * lazy loading
 * 也称懒汉式
 * 在需要实例时,才进行初始化。
 * 虽然达到了按需初始化的目的,但却带来一些的问题
 */
public class Mgr03 {
    private static Mgr03 INSTANCE;

    private Mgr03() {
    }

    public static Mgr03 getInstance() {
        //有可能线程A,B同时进行了判断,均为null,然后线程A,B都进行了初始化的操作,这样就生成了两个实例,与单例模式不符。
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr03();
        }
        return INSTANCE;
    }

    //验证,一个类中的同一个对象的hashcode是相同的,
    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->
                    System.out.println(Mgr03.getInstance().hashCode())
            ).start();
        }
    }
}

这是Mgr03的运行结果,可以看出这种写法会造成实例对象的不一致,不符合单例模式的要求。


懒汉模式不安全,对方法进行加锁可避免这个问题

/**
 * lazy loading
 * 也称懒汉式
 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
 * 可以通过synchronized解决,但也带来效率下降
 */
public class Mgr04 {
    private static Mgr04 INSTANCE;

    private Mgr04() {
    }

    public static synchronized Mgr04 getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr04();
        }
        return INSTANCE;
    }

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

结果如图:

通过给getInstance方法进行synchronized的方法来实现线程安全,但是性能方法会被大打折扣。


这时候有人想让锁的粒度更细一些,来增加性能,代码如下:

/**
 * lazy loading
 * 也称懒汉式
 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
 * 可以通过synchronized解决,但也带来效率下降
 */
public class Mgr05 {
    private static Mgr05 INSTANCE;

    private Mgr05() {
    }

    public static Mgr05 getInstance() {
        if (INSTANCE == null) {
            //妄图通过减小同步代码块的方式提高效率,然而会出现和不加锁的懒汉一样问题
            //线程A,B同时判断了INSTANCE为null,A获得了锁,new了一个出来,B等待A释放锁之后又new了一个出来。
            synchronized (Mgr05.class) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new Mgr05();
            }
        }
        return INSTANCE;
    }

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

这种减少同步代码块的方法,可以找出来问题,就是A,B线程有可能同时判断为null,A获得锁创建完对象后,由于B已经判断过INSTANCE为null,所以当B获得锁之后直接创建对象,造成了线程安全问题。既然已经知道了原因,所以在获得锁之后再加上一层判断即可,代码如下:

/**
 * lazy loading
 * 也称懒汉式
 * 通过双重判断来解决线程安全问题。
 */
public class Mgr06 {
//这里加上valatile是为了防止指令重排,
private static volatile Mgr06 INSTANCE; //JIT private Mgr06() { } public static Mgr06 getInstance() {
//这里必须加上判断,可以减少后面线程获取,判断不为null,直接往下走,不需要每一件线程都去争取锁。
if (INSTANCE == null) { //双重检查 synchronized (Mgr06.class) { if(INSTANCE == null) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } INSTANCE = new Mgr06(); } } } return INSTANCE; } public static void main(String[] args) { for(int i=0; i<100; i++) { new Thread(()->{ System.out.println(Mgr06.getInstance().hashCode()); }).start(); } } }

还有两种比较少见的方法:静态内部类的方式和枚举的方式。

静态内部类

/**
 * 静态内部类方式
 * JVM保证单例
 * 加载外部类时不会加载内部类,只有调用getInstance方法时,才会进行加载。这样可以实现懒加载
 */
public class Mgr07 {

    private Mgr07() {
    }

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

    public static Mgr07 getInstance() {
        return Mgr07Holder.INSTANCE;
    }


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


}

这个方法的线程安全是由JVM保证的。


枚举的方法:

/**
 * 不仅可以解决线程同步,还可以防止反序列化。
 */
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();
        }
    }

这是最牛逼的方法,也是最简单的方法。是《Effective Java》的作者书中说明的一种实现单例的方法。

总结

  1. 单例模式实现的思想就是构造方法私有化。
  2. 实际开发中饿汉模式就足以满足。不需要很复杂的二重判断,做到心中有剑,手中无剑。
  3. 枚举是最好的实现单例的方式。
  4. 使用双重检查实现单例时,必须交上volatile关键字,防止指令重排。

 

posted @ 2020-11-19 18:51  贼帅的火柴人  阅读(110)  评论(0编辑  收藏  举报