Java单例模式的几种写法

158 阅读4分钟

设计模式里单例模式是最面试官最喜欢问的问题,下面我将从是否懒加载(懒汉,饿汉),是否线程安全,能否防止反射构建三个角度来阐述单例模式的几种写法,并给出推荐的两种写法。

懒汉+线程不安全

//1.懒汉+线程不安全
public class Singleton {
    //私有构造方法
    private Singleton() {}  
    //单例对象
    private static Singleton instance = null;
    //静态工厂方法
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

单例的私有构造方法禁止通过构造方法新建对象,Singleton signleton = New Singleton()编译报错,只能通过静态工厂方法获取对象。当多个线程同时访问getInstance()方法,存在竞争,有可能产生多个实例,因此是线程不安全的。

这里简单介绍一下“懒汉模式”,“饿汉模式”的字面含义。 在该版本中,该单例对象是静态变量,该单例类初始化时,赋值为null,是懒加载,通常称为懒汉模式。如果该静态变量,类初始化时,赋值为new Singleton(),不是懒加载,通常称为饿汉模式

懒汉+线程安全 改造1

//2.懒汉式,线程安全
public class Singleton {
    private Singleton() {}
    private static Singleton instance;
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

针对上述线程不安全,最简单的方法就是给getInstance()方法加synchronized关键字,对其方法进行加锁,这样能够保证线程安全。给静态方法加锁,锁的是类本身,多线程情况下存在严重的性能问题。

懒汉+线程安全 改造2

// 3.懒汉式,线程安全(性能改进)
public class Singleton {
    private Singleton() {}
    private static volatile Singleton instance;
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
        }
        return instance;
    }
}

针对上述多线程性能问题,做了两个改进,1)这里对singleton变量使用volatile关键字进行修饰,volatile能够禁止JVM的指令重排序,同时一个线程对singleton变量修改其它线程能立即看到。2)将synchronized关键字从getInstance()方法上放到方法内部,只锁方法里if条件下的代码块,大大降低了多线程情况下的竞争。

饿汉+线程安全

//4.饿汉+线程安全
pulic class Singleton {
    private static Singleton singleton = new Singleton();
    private Singleton() {
    }
    public static Singleton getSingleton() {
        return singleton;
    }
}

基于classloader避免多线程同步问题,因此是线程安全的。类装载的时候就加载,运行时快,缺点是没有实现懒加载。

懒汉+线程安全(静态内部类实现)

//5.静态内部类
public class Singleton {
    private Singleton() {}
    private static SingletonHolder {
        private static final Singleton singleton = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonHolder.singleton;
    }
}

针对上述没有实现懒加载问题,这里使用静态内部类,拥有4的全部优点,并且实现了懒加载。推荐大家使用(不能防止反射构建多个实例)。

懒汉+线程安全(枚举实现)

//6.懒汉+线程安全(枚举实现)
public enum SingletonEnum {
    INSTANCE;
}

代码足够简洁,《Effective Java》作者推荐使用枚举实现单例模式。下面我们来分析一下代码,枚举类里有一个隐式的私有构造方法private Singleton () {},并且JVM 会阻止反射获取枚举类的私有构造方法。

关于反射攻击,这里补充介绍一下。

import org.testng.Assert;
import org.testng.annotations.Test;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * Created by hahazei on 2020/3/16.
 */
public class SingleTest {
    @Test
    public void test_singleton1() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class objClass = Singleton.class;
        //获取类的构造器
        Constructor constructor = objClass.getDeclaredConstructor();
        //把构造器私有权限放开
        constructor.setAccessible(true);
        //正常的获取实例方式
        Singleton singleton = Singleton.getInstance();
        //反射创建实例
        Singleton reflectSingleton = (Singleton) constructor.newInstance();

        Assert.assertEquals(singleton, reflectSingleton);//false
    }

    @Test
    public void test_singleton_enum() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        Class objClass = SingletonEnum.class;
        //获取类的构造器
        Constructor constructor = objClass.getDeclaredConstructor(); //报错java.lang.NoSuchMethodException: SingletonEnum.<init>()
        //把构造器私有权限放开
        constructor.setAccessible(true);
        //正常的获取实例方式
        SingletonEnum singletonEnum = SingletonEnum.INSTANCE;
        //反射创建实例
        SingletonEnum reflectSingletonEnum = (SingletonEnum) constructor.newInstance();

        Assert.assertEquals(singletonEnum, reflectSingletonEnum);
    }
}

test_singleton1()方法中的Singleton类是本文中前5中单例,都会报不相等,所以前5中不能防止反射构建多个实例

test_singleton_enum()方法中获取类的构造器Constructor constructor = objClass.getDeclaredConstructor();这一行就报错,无法通过放射来构建实例,因此能够防止放射攻击。推荐使用。

参考链接

1.stackoverflow.com/questions/2…

2.zhuanlan.zhihu.com/p/33102022