Java-单例模式

一、单例模式

单例模式,是一种设计模式.

单例=单个实例(对象)

某个类,在一个进程中,只应该创建出一个实例(原则上不应该有多个)

使用单例模式,就可以对代码进行一个更严格的校验和检查.

实现单例模式的方式有很多种,

此处介绍两种最基础的实现方式:

1. 饿汉模式

class Singleton{
    private static Singleton instance = new Singleton();

    public static Singleton getInstance(){
        return instance;
    }
}

这个instance,使用static创建,也就是说,instance就是Singleton类对象里面持有的属性.

这个引用,就是我们希望创建出的唯一的实例的引用.

Singleton.class (从.class文件加载到内存中,表示这个类的一个数据结构)

每个类的类对象,只存在一个.

类对象中的static属性,自然也是只有一个了.

因此,instance指向的这个对象,就是唯一uige对象.

其他代码想要使用这个类的实例,就需要通过这个方法来进行获取.

不应该在其他代码中重新new这个对象,而是使用这个方法获取到线程的对象.(创建好的对象,不是Thread)

    private Singleton(){
        
    }

当然,我们可以写上这样的一个构造方法,这样即使是想new也不行.

上述代码,就称为 " 饿汉模式 "

这是单例模式中一个非常简单的写法,所谓 " 饿 " 形容 " 非常迫切 "

实例在类加载的时候就创建了,创建的时机非常早,相当于程序一启动,实例就创建了.

ps : 

使用反射可以打破上述约定.反射属于非常规手段.

2. 懒汉模式

创建实例的时机不太一样了.

创建实例的时机会更晚,只到第一次使用的时候,才会创建实例.

private static SingletonLazy instance = null;

    public static SingletonLazy getInstance(){
        if(instance == null){
            instance = new SingletonLazy();
        }
        return instance;
    }

如上述代码,懒汉模式,只有在第一次使用的时候才会创建实例.

如果是首次调用getInstance,那么此时,instance的引用为null,那么就会进入if循环,把实例创建出来.

如果是后续再次调用getInstance,由于instance已经不再是null,此时不会进入if,直接返回之前创建好的引用.

这样子仍可以保证,该类的实例是唯一一个了.

与此同时,创建实例的时机就不是程序启动时了,而是第一次调用getInstance的时候了.

二、单例模式的线程安全

那么接下来,我们来探讨这两个模式的线程安全情况.

1. 饿汉模式   -> 线程安全

对于饿汉模式来说,getInstance 直接返回 Instance 实例.这个操作本质上就是 " 读操作 ".

多个线程读取一个变量,线程是安全的.

2. 懒汉模式   ->线程不安全   (  在多线程情况下可能会创建出多个实例  )

在懒汉模式中,代码有读也有写.

在代码的执行中,如果t1执行完if后,接着执行线程t2,由于此时还没有创建实例,所以instance依旧是null,此时t2就会创建一个实例,然后继续执行t1的if语句中的内容,就又创建了一个实例.

这就导致实例被new了两次,这就不是单例模式了.

那么.如何改进懒汉模式,让他称为线程安全的代码呢?

 ->  synchronized   加锁

我们可以把代码进行这样的改进

 private static SingletonLazy instance = null;
    public static Object locker = new Object();
    public static SingletonLazy getInstance(){
        if(instance == null) {
            synchronized (locker) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

在原先的if循环外,我们使用了synchronized进行加锁

这个加锁要在if的外面,因为要使if和创建实例是原子的,不然还是有可能发生线程安全问题.

并且,在synchronized外又嵌套了一层if,这是为了在第一次创建实例后,如果还是每次都进行加锁,进程的效率就会变低.

加锁意味着可能产生阻塞,一旦阻塞,什么时候能解除就不知道了.

所以,在锁的外面又使用了一次if,在需要加锁的时候才加锁.

第一个 if 判定的是是否要加锁.

第二个 if 判定的是是否要创建对象.

这两个if的条件相同   ->   线程安全&执行效率   ->   " 双重校验锁 "

三、指令重排序引起的线程安全问题

3.1 指令重排序 引起的 线程安全问题

但是.这个代码还是有一些问题   ->   指令重排序 引起的 线程安全问题

指令重排序,也是编译器优化的一种方式.

        调整原有代码的执行顺序,保证逻辑不变的前提下,提高程序的效率.

那么   这一个代码

instance = new SingletonLazy();     ->   可以拆分为三个大的步骤.( 不是指令 )

1. 申请一段内存空间

2. 在这个内存上调用构造方法,创建出这个实例

3. 把这个内存地址赋值给Instance 引用变量

正常是123的,但编译器也会优化成132.

结果都是instance拿到这个内存地址,但是可能会出现没有调用构造方法创建实例就直接赋值地址.

这就会导致,使用这个实例里面的属性和方法会出现错误.因为此时的instance的属性是一个未初始化的, " 全 0 的值 ".

3.2 利用volatile解决线程安全问题

解决这个问题的核心思路,就是利用volatile

volatile有两个功能 :

1. 保证内存可见性,每次访问变量必须都要重新读取内存,而不会优化到寄存器/缓存中.

2. 禁止指令重排序,针对这个被volatile修饰的变量的读写操作相关指令,是不能被重排序的.

private volatile static SingletonLazy instance = null;

  • 16
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值