单例模式(Singleton)
特点:
- 类构造器私有
- 持有自己类型的属性
- 对外提供获取实例的静态方法
总之,单例模式就是为确保一个类只有一个实例,并为整个系统提供一个全局访问点的一种方法。
饿汉模式
饿汉式天生就是线程安全的,比较常用,但容易产生垃圾,因为一开始就要初始化
// 饿汉式单例
public class Hungry {
private final static Hungry HUNGRY = new Hungry();
private Hungry() {}
public static Hungry getInstance() {
return HUNGRY;
}
}
懒汉模式
线程不安全!
// 懒汉式单例,多线程测试
public class Lazy {
private Lazy() {
System.out.println(Thread.currentThread().getName() + "ok");
}
private static Lazy Lazy;
public static Lazy getInstance() {
if (Lazy == null) {
Lazy = new Lazy();
}
return Lazy;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
getInstance();
}).start();
}
}
}
总之,从速度和反应时间角度来讲,饿汉式(又称立即加载)要好一些;从资源利用效率上说,懒汉式(又称延迟加载)要好一些。
在单线程环境下,两种模式都能正常工作。但在多线程环境下,饿汉式天生线程安全,所以不会出现问题,但懒汉式有可能出现多个实例的情况
DCL懒汉式(双重锁模式)
线程安全,双重加载,延迟初始化
package DesignPatterns;
// 懒汉式单例
public class DCLazy {
private DCLazy() {
System.out.println(Thread.currentThread().getName() + "ok");
}
// 一定要加volatile!!
private volatile static DCLazy Lazy;
// 双重检测锁模式的懒汉式单例,DCL懒汉式
public static DCLazy getInstance() {
if (Lazy == null) {
synchronized (DCLazy.class) {
if (Lazy == null) {
// 如果不是volatile,那就原子性操作(会有指令重排)
// 1.分配内存空间 2.执行构造方法初始化对象 3.把这个对象指向这个空间
Lazy = new DCLazy();
}
}
}
return Lazy;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(DCLazy::getInstance).start();
}
}
}
问题一:为什么饿汉式天生线程安全?
类加载的方式是按需加载,且只加载一次。 因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用。
换句话说,在线程访问单例对象之前就已经创建好了。再加上,一个类在整个生命周期中只会被加载一次,因此该单例类只会创建一个实例,也就是说,线程每次都只能也必定只可以拿到这个唯一的对象。因此就说,饿汉式单例天生就是线程安全的。
问题二:为什么懒汉式线程不安全
在执行过程中,可能会有多个线程同时进行 Lazy == null
的判断,这种情况就也可能会创建出多个实例,违背单例模式初衷。
同步延迟加载——内部类
私有内部类,按需加载,也就是延迟加载
// 静态内部类
public class Holder {
private Holder() {
System.out.println(Thread.currentThread().getName() + "新建实例");
}
private static Holder getInstance() {
return InnerClass.HOLDER;
}
private static class InnerClass {
private static final Holder HOLDER = new Holder();
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(Holder::getInstance).start();
}
}
}
问题三:为什么要用枚举类实现单例模式?
在《Effective Java》中提到了几句话:“享有特权的客户端可以借助 AccessibleObject.setAccessible 方法,通过反射机制调用私有构造器。如果需要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。”
// 枚举单例的实现
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
// enum 是一个什么? 本身也是一个Class类
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}