设计模式之单例模式

简介

单例模式可以说是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 提倡的方式。

总结

单例模式看似简单,其实充满细节。
单例模式分为饿汉式和懒汉式两种,需要按需来选择。
而线程安全的有饿汉式,双重校验锁的懒汉式、静态内部类的懒汉式、以及枚举实现的单例模式。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值