1 解决的问题

  • 频繁创建增加内存+时间。

2 单例模式(Singleton Pattern)

单例模式非常常见,某个对象全局只需要一个实例时,就可以使用单例模式。它的优点也显而易见:

  • 它能够避免对象重复创建,节约空间并提升效率
  • 避免由于操作不同实例导致的逻辑错误

单例模式有两种实现方式:饿汉式和懒汉式。

2.1 实现方式

2.1.1 实现 1:饿汉式

概念:变量在声明时便初始化。

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() { }
    public static Singleton getInstance() {
        return instance;
    }
}

特点:

  1. 构造方法为 private,保证其他类无法实例化此类。
  2. 弊端:类加载后立即创建出来,占用内存和初始化时间。
  3. 等待被调用,被称为饿汉式。
  4. 优点:线程安全。

2.1.2 实现 2:懒汉式

概念:需要用时再初始化。

public class Singleton {
    private static Singleton instance = null;
    private Singleton() {     }
    public static Singleton getInstance(){
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

特点:

  1. 优点:按需加载,避免内存浪费,降低类初始化时间。
  2. 弊端:多线程不安全。

2.1.3 实现 3:懒汉式-加锁

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

特点:执行效率低。

2.1.4 实现 4:懒汉式-双检锁(推荐)

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

一个单例模式中volatile关键字引发的思考 - 掘金

instance = new Singleton(); 正常顺序如下:(1)分配内存、(2)初始化对象、(3)更新地址。
但是可能 JVM 会指令重排,变成(1)分配内存、(3)更新地址、(2)初始化对象。当执行完(3)后,若线程 B 对对象进行判断,发现对象不为 null,那么就会返回一个未初始化的对象,从而造成错误。

2.1.5 实现 5:静态内部类(推荐)

public class Singleton {
    private static class SingletonHolder {
        public static Singleton instance = new Singleton();
    }
    private Singleton() {     }
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

更深层次问题:

  • 静态内部类方式是怎么实现懒加载的
  • 静态内部类方式是怎么保证线程安全的

Java 类的加载过程包括:加载、验证、准备、解析、初始化。初始化阶段即执行类的 clinit 方法(clinit = class + initialize),包括为类的静态变量赋初始值和执行静态代码块中的内容。但不会立即加载内部类,内部类会在使用时才加载。所以当此 Singleton 类加载时,SingletonHolder 并不会被立即加载,所以不会像饿汉式那样占用内存。

另外,Java 虚拟机规定,当访问一个类的静态字段时,如果该类尚未初始化,则立即初始化此类。当调用 Singleton 的 getInstance 方法时,由于其使用了 SingletonHolder 的静态变量 instance,所以这时才会去初始化 SingletonHolder,在 SingletonHolder 中 new 出 Singleton 对象。这就实现了懒加载。

第二个问题的答案是 Java 虚拟机的设计是非常稳定的,早已经考虑到了多线程并发执行的情况。虚拟机在加载类的 clinit 方法时,会保证 clinit 在多线程中被正确的加锁、同步。即使有多个线程同时去初始化一个类,一次也只有一个线程可以执行 clinit 方法,其他线程都需要阻塞等待,从而保证了线程安全。

2.1.6 实现 6:枚举(推荐)

public enum  EnumSingleton {
    INSTANCE;
    public EnumSingleton getInstance(){
        return INSTANCE;
    }
}
public class User {
    private User(){ }
    static enum SingletonEnum{
        INSTANCE;  //创建一个枚举对象,该对象天生为单例
        private User user;
        private SingletonEnum(){        //私有化枚举的构造函数
            user=new User();
        }
        public User getInstnce(){
            return user;
        }
    }
    public static User getInstance(){  //对外暴露一个获取User对象的静态方法
        return SingletonEnum.INSTANCE.getInstnce();
    }
}
 
public class Test {
    public static void main(String [] args){
        System.out.println(User.getInstance());
        System.out.println(User.getInstance());
        System.out.println(User.getInstance()==User.getInstance());
    }
}

2.2 其他安全问题

除了 线程安全问题 限制创建对象,还有其他创建对象的方式:克隆反射序列化

2.2.1 创建对象 1:克隆

要克隆一个类,需要(1)被克隆类实现 Cloneable 接口,(2)重写 clone() 函数。一般单例不会实现该接口,不存在破坏问题。

2.2.2 创建对象 2:反射

设置 Flag 标志位解决,但是通过反射修改 Flag 标志位同样还是会破坏。

public class Singleton {
    // 私有构造方法
    private Singleton() {
        /*
           反射破坏单例模式需要添加的代码
        */
        if(instance != null) {
            throw new RuntimeException();
        }
    }
    
    private static volatile Singleton instance;
 
    // 对外提供静态方法获取该对象
    public static Singleton getInstance() {
        if(instance != null) {
            return instance;
        }
 
        synchronized (Singleton.class) {
            if(instance != null) {
                return instance;
            }
            instance = new Singleton();
            return instance;
        }
    }
}
 

2.2.3 创建对象 3:序列化

public class SingletonTest {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = null;
        // 序列化
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
            oos.writeObject(singleton1);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 反序列化
        try {
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file"));
            try {
                singleton2 = (Singleton) ois.readObject();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println(singleton1 == singleton2);   // 输出:false
    }
}
 

通过从 readObject 跟到 readOrdinaryObject,定位到下述代码:

  • ** isInstantiable() **:一个 serializable/externalizable 的类是否可以在运行时被实例化;
  • ** desc.newInstance() **:通过反射的方式调用无参构造函数创建一个新对象;

  • ** desc.hasReadResolveMethod() **:判断类是否实现了 readResolve()函数;
  • ** desc.invokeReadResolve(obj) **:有的反射调用此函数,如果在此函数中返回实例就可以了;

修改后的单例类代码:

import java.io.Serializable;
public class Singleton implements Serializable {
    private Singleton() { }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Object readResolve() {
        return getInstance();
    }
}

2.3 枚举单例

安全简单,没有懒加载,最佳实践

public enum SingletonEnum {
    INSTANCE;
    private final AtomicLong id = new AtomicLong(0);
 
    public long getId() {
        return id.incrementAndGet();
    }
}
 
// 调用
SingletonEnum.INSTANCE.getId()
 

2.4 应用场景

  1. 处理资源访问冲突
    日志工具类,多个实例,多线程并发写入可能存在互相覆盖的情况。

  2. 全局唯一类
    数据在系统中只保存一份,如:用来封装全局配置信息的类

QQ 和微信,消息内容属于即时加载,打开软件时就加载各个聊天内容;朋友圈属于懒汉式加载。

《代码整洁之道》一书中也说到:不提倡使用懒加载方式,因为程序应该将构建与使用分离,达到解耦。饿汉式在声明时直接初始化变量的方式也更直观易懂。所以在使用饿汉式还是懒汉式时,需要权衡利弊。

一般的建议是:对于构建不复杂,加载完成后会立即使用的单例对象,推荐使用饿汉式。对于构建过程耗时较长,并不是所有使用此类都会用到的单例对象,推荐使用懒汉式。

TODO

把书读薄 | 《设计模式之美》设计模式与范式(创建型-单例模式) - 掘金