设计模式之单例模式
单例模式(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》的作者书中说明的一种实现单例的方法。
总结
- 单例模式实现的思想就是构造方法私有化。
- 实际开发中饿汉模式就足以满足。不需要很复杂的二重判断,做到心中有剑,手中无剑。
- 枚举是最好的实现单例的方式。
- 使用双重检查实现单例时,必须交上volatile关键字,防止指令重排。