简介
单例模式可以说是Java常用的23种设计模式中,最为简单的一种,应用也十分广泛,实现方式也很多。很多框架都使用到了单例模式,比如mybatis的SqlSessionFactory就是一个单例模式的应用,以及JDK中Runtime这个类,也是单例模式,对于一些经常创建销毁的对象,以及系统中只需要一份的对象,我们就可以使用单例模式。单例模式又分为饿汉式和懒汉式。
饿汉式
饿汉式,字如其名,就是上来给你提供一个实例。最常见最基础的饿汉式代码如下(静态变量):
public class Singleton {
private Singleton() {} // 构造器私有化
private static final Singleton INSTANCE = new Singleton(); // 内部创建实例
public static Singleton getInstance() { // 暴露给外部的公有方法
return INSTANCE;
}
}
思想就是将构造器私有化,不然外部能够通过 new 来创建,在内部创建好实例,提供一个静态方法给外部调用获得实例。这里因为是静态变量赋值,会在类加载的时候创建对象,且只会加载一次,故在创建对象时,不存在线程安全问题。
使用静态代码块也可以实现如上操作,并且可以在静态代码块中进行一些初始化工作。
class Singleton {
private String username;
private static final Singleton INSTANCE;
static {
// 可以在静态块中做一些其他的操作,比如说读取配置类啥的
Properties info = new Properties();
InputStream is = Singleton.class.getClassLoader().getResourceAsStream("druid.properties");
try {
info.load(is);
} catch (IOException e) {
e.printStackTrace();
}
INSTANCE = new Singleton(info.getProperty("username"));
}
private Singleton(String userName) {
this.username = userName;
}
public static Singleton getInstance() {
return INSTANCE;
}
public String getUserName() {
return username;
}
}
测试
测试饿汉式之前,先将饿汉式的代码小小的改造一下,使得看起来更加直观,因为静态变量赋值和静态代码块赋值效果都一样,我这里就是用静态变量赋值方式的饿汉式来测试。
改造:
public class Singleton {
private static int count = 0; // 静态类型 count ,用来记录构造器被调用几次
public static final Singleton INSTANCE = new Singleton();
private Singleton() {
System.out.println("调用了" + ++count + "次"); // 输出结果
}
public static Singleton getInstance() {
return INSTANCE;
}
}
测试结果如下:
public class SingletonTest {
public static void main(String[] args) {
for (int i = 0; i < 3000; i++) {
new Thread(()-> {
Singleton s = Singleton.getInstance();
}).start();
}
}
}
可以看到,即使在多线程情况下,饿汉式也能保证是线程安全的,保证是单例的。但饿汉式并没有实现 懒加载 的效果,并且类加载的时机有很多种,如果由于其他原因导致了类加载,而我们从始至终都没有使用过这个单实例,那么就会造成内存的浪费。因此,出现了懒汉式。
懒汉式
懒汉式,字如其名,很懒,只在你需要的时候提供实例。
基本的懒汉式代码如下:
public class Singleton {
private Singleton() { // 构造器私有化
}
private static Singleton instance; // 声明静态变量
public static Singleton getInstance() {
if (instance== null) { // 判断实例是否存在
instance= new Singleton(); // 创建实例
}
return instance;
}
}
懒汉式的基本思想就是在获取实例时,先判断该实例是否已经存在,如果存在,则返回存在的实例,如不存在,再去创建,实现了懒加载的功能。
在多线程情况下,懒汉式并非线程安全的。
还是和上面一样,改造后,测试代码如下:
public class SingletonTest {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
Singleton s = Singleton.getInstance();
}).start();
}
}
}
可以看到,直接创建了三个实例,单例变多例了。所以们需要对懒汉式进行改造。
那么保证线程安全最常用的方式就是上锁。所以我们可以在 getInstance() 方法上面加锁,其他都不变。如下所示:
public synchronized static Singleton getInstance() {
if (instance== null) {
instance= new Singleton();
}
return instance;
}
再次测试:
可以看到,构造方法只被调用了一次。但是synchronized粒度比较大,本来只需要一个 if 判断的事情,现在却要让其他没抢到锁的线程全部都在外面等着,效率有点小低。所以我们可以将synchronized放到if判断里面,如下所示:
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
但是这样也有问题的,我们可以先理论上分析下。当并发情况下,大量线程访问getInstance()方法,此时进入if判断,第一个线程if判断为true成立,然后要进行实例化对象了。然后在对象还没实例化好之前,其他线程也进入到了if判断,此时对象还没有创建完成,故if判断为true也成立,也进入到了if判断里面,随后抢占锁创建对象,故不是多例。
测试结果如下:
可以看到,该实例被创建了7次。所以我们需要在synchronized同步代码块中在进行一次判断,这样抢占到锁的线程创建完对象后,其他线程再去if判断是否为null,就不会为true,则不会创建对象。
具体代码如下:
public static Singleton getInstance() {
if (instance == null) { // 一重校验
synchronized (Singleton.class) {
if (instance == null) { // 二重校验
instance = new Singleton();
}
}
}
return instance;
}
这也就是人们常说的双重校验锁,也称为DCL(double checked locking)。这也是开发中常用的模式。
现在看似没有什么问题,其实还有一个细节没有考虑到。就发生在 instance = new Singleton();这行代码这里。我们都知道并发情况的三大问题:缓存带来的可见性问题、线程切换带来的原子性问题、编译器优化带来的有序性问题。 这里我们就需要考虑有序性问题。
instance = new Singleton();分为三步。第一:开辟地址空间。第二:属性赋值。第三:引用指向地址然而,编译器执行的时候,并不一定严格按照1、2、3的步骤去执行。可能1、3、2了。那么这样返回的对象,就会造成数据丢失问题。
这个现象博主我试了很久,一直没有尝试出来,但是理论上应该是存在的。我们可以通过加一个volatile关键字来解决。volatile可以禁止指令重排,让指令按照1、2、3的顺序执行,并能立即刷新主存内存,保证内存值是立即可见的。
双重校验锁完整代码如下:
public class Singleton {
private Singleton() {
}
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
还可以使用静态内部类来实现懒汉式
使用静态内部类也可以实现懒汉式的懒加载效果,因为外部类加载时,静态内部类并不会被加载,除非我们调用静态内部类里面的静态方法或者属性。
代码如下:
public class Singleton {
private Singleton() {
}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
枚举实现
使用枚举也可以实现单例模式的效果。
代码如下:
enum Singleton {
INSTANCE;
}
嗯,就是这么简单,需要实例时只需要 Singleton.INSTANCE即可。且使用枚举还可以避免反射和序列话的破坏,不会创建两个对象,而是抛出异常。
源码如下:
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
throw new InvalidObjectException("can't deserialize enum");
}
private void readObjectNoData() throws ObjectStreamException {
throw new InvalidObjectException("can't deserialize enum");
}
另外,枚举方式也是Effective Java作者Josh Bloch 提倡的方式。
总结
单例模式看似简单,其实充满细节。
单例模式分为饿汉式和懒汉式两种,需要按需来选择。
而线程安全的有饿汉式,双重校验锁的懒汉式、静态内部类的懒汉式、以及枚举实现的单例模式。