转载

Java设计模式优化-单例模式

单例模式概述

单例模式是一种对象创建模式,用于产生一个类的具体事例。使用单例模式可以确保整个系统中单例类只产生一个实例。有下面两大好处:

  1. 对于频繁创建的对象,节省初第一次实例化之后的创建时间。
  2. 由于new操作的减少,会降低系统内存的使用频率。减轻GC压力,从而缩短GC停顿时间

创建方式:

  1. 单例作为类的私有private属性
  2. 单例类拥有私有private构造函数
  3. 提供获取实例的public方法

单例模式的角色:

角色 作用
单例类 提供单例的工厂,返回类的单例实例
使用者 获取并使用单例类

类基本结构:

Java设计模式优化-单例模式

单例模式的实现

1.饿汉式

public class HungerSingleton {
    //1.饿汉式
    //私有构造器
    private HungerSingleton() {
        System.out.println("create HungerSingleton");
    }
    //私有单例属性
    private static HungerSingleton instance = new HungerSingleton();
    //获取单例的方法
    public static HungerSingleton getInstance() {
        return instance;
    }
}

注意:

  1. 单例修饰符为static JVM加载单例类加载时,直接初始化单例。无法延时加载。如果此单例一直未被使用,单Singleton 因为调用静态方法被初始化则会造成内存的浪费。
  2. getInstance()使用static修饰,不用实例化可以直接使用Singleton.getInstance()获取单例。
  3. 由于单例由JVM加载类的时候创建,所以不存在 线程安全 问题。

2.简单懒汉式

public class Singleton {
    //2.1简单懒汉式(线程不安全)
    //私有构造器
    private Singleton() {
        System.out.println("create Singleton");
    }
    //私有单例属性[初始化为null]
    private static Singleton instance = null;
    //获取单例的方法
    public static Singleton getInstance() {
        if(instance == null) {
            //此处instance实例化
            //首次调用单例时会进入  达成延时加载
            instance = new Singleton();
        }
        return instance;
    }
}
  • 由于未使用 synchronized 关键字,所以当线程1调用单例工厂方法Singleton.getInstance() 且 instance 未初始化完成时,线程2调用此方法会将instance判断为null,也会将instance重新实例化赋值,此时则产生了多个实例!
  • 如需线程安全可以直接给getInstance方法上加synchronized关键字,如下:
public class Singleton {
    //2.2简单懒汉式(线程安全)
    //私有构造器
    private Singleton() {
        System.out.println("create Singleton");
    }
    //私有单例属性[初始化为null]
    private static Singleton instance = null;
    //获取单例的方法 将此方法使用synchronized关键字同步
    public static synchronized Singleton getInstance() {
        if(instance == null) {
            //此处instance实例化
            //首次调用单例时会进入  达成延时加载
            instance = new Singleton();
        }
        return instance;
    }
}

面临的问题:

  • 由于对getInstance()整个方法加锁,在多线程的环境中性能比较差。

3.DCL 懒汉式(双重检测)

简单懒汉式(线程安全)中,对getInstance()方法加锁,导致多线程中性能较差,那么是否可以 减小锁的范围

,使不用每次调用geInstance()方法时候都会去竞争锁?

DCL(Double Check Locking)双重检测 就是这样一种实现方式。

传统DCL:

public class DCLLazySingleton {
    //3.DCL
    //私有构造器
    private DCLLazySingleton() {
        System.out.println("create DCLLazySingleton");
    }
    //step1 私有单例属性[初始化为null] volatile 保证内存可见性 防止指令重排
    private static volatile DCLLazySingleton instance = null;
    //获取单例的方法
    public static DCLLazySingleton getInstance() {
        //这里判null 是为了在instance有值时,不进入加锁的代码块,提高代码性能。
        if(instance == null) {
            //缩小锁范围 由于是静态方法方法调用的时候不依赖于实例化的对象 加锁只能使用类
            synchronized (DCLLazySingleton.class) {
                //这里判null 是为了配合volatile解决多线程安全问题
                if(instance == null) {
                    instance = new DCLLazySingleton();
                }
            }
        }
        return instance;
    }
}

注意:

  1. 传统DCL(未使用 volatile 或在JDK1.8之前版本)面临的问题:

    • 由于初始化单例对象new DCLLazySingleton() 操作 并不是原子操作 ,由于这是很多条指令,jvm可能会乱序执行。

在线程1初始化对象可能并未完成,但是此时已经instance对象已经不为null。(已经分配了内存,但是构造方法还未执行完【 可能有一些属性的赋值未执行 】)

此时线程2再获取instance 则不为null 直接返回。那么此时线程2获取的则为‘构造方法未执行完的instance对象’。则不能保证线程安全。

  1. 解决方式:

    • 加上volatile关键字,volatile保证内存可见性,内存屏障,防止指令排!
    • 加上volatile关键字后,线程2获取的构造方法未执行完的instance对象,会在线程1修改之后同步到线程2(volatile 内存空间)。所以解决了线程安全问题
  2. 参考:

    • DCL失效原因和解决方案
    • java 中单例模式DCL的缺陷及单例的正确写法

4.懒汉式(静态内部类)

public class StaticSingleton {
    //私有构造器
    private StaticSingleton() {
        System.out.println("create StaticSingleton!");
    }
    //获取单例的方法
    public static StaticSingleton getInstance() {
        return SingletonHolder.instance;
    }
    //静态内部类 持有单例 作为静态属性。
    //由于只有在访问属性时才会加载静态类初始化instance。所以实现了懒加载。且由于JVM保证了类的加载为线程安全,所以为线程安全的。
    private static class SingletonHolder {
        //私有单例属性
        private static StaticSingleton instance = new StaticSingleton();
    }
}

注意:

  1. 由于StaticSingleton类被加载时,内部的私有静态类SingletonHolder并不会被加载,所以并不会初始化单例instance,当getInstance()被调用时SingletonHolder.instance 才会加载SingletonHolder,由于JVM保证了类的加载为线程安全,因此线程安全。
  2. 此方式既可以做到延时加载,也不会因为同步关键字影响性能。是一种比较完善的实现。 推荐使用

5.枚举单例

public enum EnumSingleton {
    INSTANCE();
    EnumSingleton() {
        System.out.println("create EnumSingleton");
    }
}
  • 线程安全,且能够抵御反射与序列化。
  • 推荐使用

例外情况

上述的单例实现方式还是会面临一些特殊情况不能保证唯一实例:

  1. 反射调用私有构造方法。
  2. 序列化后反序列化会生成多个对象。可以实现私有readResolve方法。readObject()如同虚设,直接使用readResolve替换原本返回值。如下:
private Object readResolve () {
    //返回当前对象
    return instance;
    }

由于上述两情况比较特殊,所以没有特别关注。

参考书籍

《Java程序性能优化》 -葛一鸣 等编著

原文  https://segmentfault.com/a/1190000020002054
正文到此结束
Loading...