java静态内部类单例的好处,你确定你的单例模式真的用对了?

一、什么是单例模式

单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。该类负责创建自己的对象,同时确保只有一个对象被创建。一般常用在工具类的实现或创建对象需要消耗资源的业务场景。

单例模式的特点:

1.类构造器私有

2.持有自己类的引用

3.对外提供获取实例的静态方法

我们先用一个简单示例了解一下单例模式的用法。public class SimpleSingleton {

//持有自己类的引用

private static final SimpleSingleton INSTANCE = new SimpleSingleton();

//私有的构造方法

private SimpleSingleton() {

}

//对外提供获取实例的静态方法

public static SimpleSingleton getInstance() {

return INSTANCE;

}

public static void main(String[] args) {

System.out.println(SimpleSingleton.getInstance().hashCode());

System.out.println(SimpleSingleton.getInstance().hashCode());

}

}

打印结果:1639705018

我们看到两次获取SimpleSingleton实例的hashCode是一样的,说明两次调用获取到的是同一个对象。

可能很多朋友平时工作当中都是这么用的,但是我要说的是这段代码其实是有问题的。

1460000023853586private static final SimpleSingleton INSTANCE = new SimpleSingleton();

一开始就实例化对象了,如果实例化过程非常耗时,并且最后这个对象没有被使用,不是白白造成资源浪费吗?

这个时候你也许会想到,如果在真正使用的时候再实例化不就可以了?这就是我接下来要介绍的 懒汉模式。

二、饿汉模式与懒汉模式

什么是饿汉模式?

实例在初始化的时候就已经建好了,不管你有没有用到,都先建好了再说。好处是没有线程安全的问题,坏处是浪费内存空间。代码如下:public class SimpleSingleton {

//持有自己类的引用

private static final SimpleSingleton INSTANCE = new SimpleSingleton();

//私有的构造方法

private SimpleSingleton() {

}

//对外提供获取实例的静态方法

public static SimpleSingleton getInstance() {

return INSTANCE;

}

public static void main(String[] args) {

System.out.println(SimpleSingleton.getInstance().hashCode());

System.out.println(SimpleSingleton.getInstance().hashCode());

}

}

什么是懒汉模式?

顾名思义就是实例在用到的时候才去创建,“比较懒”,用的时候才去检查有没有实例,如果有则返回,没有则新建。代码如下:public class SimpleSingleton2 {

private static SimpleSingleton2 INSTANCE;

private SimpleSingleton2() {

}

public static SimpleSingleton2 getInstance() {

if (INSTANCE == null) {

INSTANCE = new SimpleSingleton2();

}

return INSTANCE;

}

public static void main(String[] args) {

System.out.println(SimpleSingleton2.getInstance().hashCode());

System.out.println(SimpleSingleton2.getInstance().hashCode());

}

}

示例中的INSTANCE对象一开始是空的,在调用getInstance方法才会真正实例化。

如果代码可能有些朋友在使用,但是还是有问题。

有什么问题呢?

假如有多个线程中都调用了getInstance方法,那么都走到 if (INSTANCE == null) 判断时,可能同时成立,因为INSTANCE初始化时默认值是null。这样会导致多个线程中同时创建INSTANCE对象,即INSTANCE对象被创建了多次,违背了一个INSTANCE对象的初衷。

要如何改进呢?

最简单的办法就是使用synchronized关键字,改进后的代码如下:public class SimpleSingleton3 {

private static SimpleSingleton3 INSTANCE;

private SimpleSingleton3() {

}

public synchronized static SimpleSingleton3 getInstance() {

if (INSTANCE == null) {

INSTANCE = new SimpleSingleton3();

}

return INSTANCE;

}

public static void main(String[] args) {

System.out.println(SimpleSingleton3.getInstance().hashCode());

System.out.println(SimpleSingleton3.getInstance().hashCode());

}

}

这样总可以了吧?

不好意思,还是有问题。

1460000023853587

有什么问题?

使用synchronized关键字会消耗性能,我们应该判断INSTANCE为空时才加锁,而不为空不应该加锁,需要直接返回。这就需要使用双重检查锁。

饿汉模式 和 懒汉模式 各有什么优缺点?

饿汉模式:好处是没有线程安全的问题,坏处是浪费内存空间。

懒汉模式:好处是没有内存空间浪费的问题,但是控制不好实际不是单例。

三、双重检查锁

双重检查锁顾名思义会检查两次,在加锁之前检查一次是否为空,加锁之后再检查一次是否为空。代码如下:public class SimpleSingleton4 {

private static SimpleSingleton4 INSTANCE;

private SimpleSingleton4() {

}

public static SimpleSingleton4 getInstance() {

if (INSTANCE == null) {

synchronized (SimpleSingleton4.class) {

if (INSTANCE == null) {

INSTANCE = new SimpleSingleton4();

}

}

}

return INSTANCE;

}

public static void main(String[] args) {

System.out.println(SimpleSingleton4.getInstance().hashCode());

System.out.println(SimpleSingleton4.getInstance().hashCode());

}

}

在加锁之前判断是否为空,可以确保INSTANCE不为空的情况下,不用加锁,可以直接返回。

为什么在加锁之后,还需要判断INSTANCE是否为空呢?

其实,是为了防止在多线程并发的情况下,比如:线程a 和 线程b同时调用

getInstance,同时判断INSTANCE为空,则同时进行抢锁。假如线程a先抢到锁,开始执行synchronized关键字包含的代码,此时线程b处于等待状态。线程a创建完新实例了,释放锁了,此时线程b拿到锁,进入synchronized关键字包含的代码,如果没有再判断一次INSTANCE是否为空,则可能会重复创建实例。

不要以为这样就完了,还有问题呢?

1460000023853588

有啥问题?public static SimpleSingleton4 getInstance() {

if (INSTANCE == null) {//1

synchronized (SimpleSingleton4.class) {//2

if (INSTANCE == null) {//3

INSTANCE = new SimpleSingleton4();//4

}

}

}

return INSTANCE;//5

}

getInstance方法的这段代码,我是按1、2、3、4、5这种顺序写的,希望也按这个顺序执行。但是java虚拟机实际上会有一些优化,对一些代码指令进行重排。重排之后的顺序可能就变成了:1、3、2、4、5,这样在多线程的情况下同样会创建多次实例。重排之后的代码可能如下:public static SimpleSingleton4 getInstance() {

if (INSTANCE == null) {//1

if (INSTANCE == null) {//3

synchronized (SimpleSingleton4.class) {//2

INSTANCE = new SimpleSingleton4();//4

}

}

}

return INSTANCE;//5

}

原来如此,那有什么办法可以解决呢?

可以在定义INSTANCE是加上volatile关键字,代码如下:public class SimpleSingleton7 {

private volatile static SimpleSingleton7 INSTANCE;

private SimpleSingleton7() {

}

public static SimpleSingleton7 getInstance() {

if (INSTANCE == null) {

synchronized (SimpleSingleton7.class) {

if (INSTANCE == null) {

INSTANCE = new SimpleSingleton7();

}

}

}

return INSTANCE;

}

public static void main(String[] args) {

System.out.println(SimpleSingleton7.getInstance().hashCode());

System.out.println(SimpleSingleton7.getInstance().hashCode());

}

}

volatile 关键字可以保证多个线程的可见性,但是不能保证原子性。同时它也能禁止指令重排。

双重检查锁的机制既保证了线程安全,又比直接上锁提高了执行效率,还节省了内存空间。

除了上面的单例模式之外,还有没有其他的单例模式?

四、静态内部类

静态内部类顾名思义是通过静态的内部类来实现单例模式的。public class SimpleSingleton5 {

private SimpleSingleton5() {

}

public static SimpleSingleton5 getInstance() {

return Inner.INSTANCE;

}

private static class Inner {

private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();

}

public static void main(String[] args) {

System.out.println(SimpleSingleton5.getInstance().hashCode());

System.out.println(SimpleSingleton5.getInstance().hashCode());

}

}

我们看到在SimpleSingleton5类中定义了一个静态的内部类Inner,SimpleSingleton5类的getInstance方法返回的是内部类Inner的实例INSTANCE。

只有第一次调用getInstance方法时,虚拟机才加载 Inner 并初始化INSTANCE ,只有一个线程可以获得对象的初始化锁,其他线程无法进行初始化,保证对象的唯一性。

五、枚举

枚举是天然的单例,每一个实例只有一个对象,这是java底层内部机制保证的。public enum SimpleSingleton6 {

INSTANCE;

public void doSameThing() {

}

}

但是实际情况下,枚举的单例用的并不多,因为它不好理解。

六、总结​

本文主要介绍了:

饿汉模式、懒汉模式、双重检查锁、静态内部类 和 枚举 这5种单例模式,各有优缺点,静态内部类是所有单例模式中最推荐的模式。

如果您看了这篇文章觉得有所收获,帮忙关注一下我的公众账号:苏三说技术。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值