多线程代码案例之单例模式

目录

单例模式

饿汉模式 

懒汉模式

问题一 

问题二 

问题三


单例模式

单例模式,是设计模式的一种。在有些特定场景中,有的特定的类,只能创建出一个实例,不应该创建多个实例。单例模式就可以保证这样的需求。例如JDBC中的DataSource就适用于单例模式。常见的实现单例模式的方式有:饿汗模式和懒汉模式。

饿汉模式 

使用 Singleton 类来创建对象。

private static Singleton instance = new Singleton();

用 static 来修饰,因此 instance的属性与实例无关,而是与类相关。由于类对象在一个java进程中,只有唯一的一份,因此类对象内部的 类属性 也是唯一的一份。

同时,这个类对象的创建是在类加载过程创建的,类加载对于运行过程来说是比较靠前的阶段,这就给人一种 “十分急切” 的感觉,因此也就称为饿汉模式。(因此也可以理解为饿汉模式也就是创建对象特别早)

(java代码中的每一个类,都会在编译完成后得到唯一的 .class 文件。当 jvm 运行的时候,就会加载这个 .class 文件读取其中的二进制指令,并且在内存中构造出对应的类对象,形如下述代码中的 Singleton.class)

static保证了这个实例的唯一性:

1.static使 instance 属性是类属性,类属性是对类对象而言的,类对象又是唯一实例的(在类加载阶段被创造出的一个实例)

2.构造方法是设为 private,因此外面的代码中无法new。

(运行一个java程序,就需要让java进程能够找到并读取对应的 .class 文件。就会读取文件内容,并解析,构造成类对象.....这一系列的过程,也称为类加载过程。要执行 java 程序的前提就是得把类加载起来)

在保证了这个实例的唯一性的同时,也保证了这个实例在一个比较早的时机被创建。

实际上类对象本身和 static 没有关系,而是类里面使用 static 修饰的成员,会作为类属性。也就相当于这个属性对应的内存空间在类对象里。

// 饿汉模式的 单例模式 的实现
// 此处保证 Singleton 这个类只能创建出一个实例
class Singleton{
    //此处先把实例创建出来
    private static Singleton instance = new Singleton();

    // 如果需要使用这个唯一实例,统一通过 Singleton.getInstance() 来获取
    public static Singleton getInstance(){
        return instance;
    }

    // 为了避免 Singleton 类不小心被复制多份出来
    // 把构造方法设为 static ,在类外面,就无法通过 new 的方式来创建这个 Singleton 实例了!
    private Singleton(){}
}

public class ThreadDemo19 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);
    }
}

在饿汉模式中,如果是多线程调用,只涉及到 “读操作” ,因此是没有线程安全问题的。 

懒汉模式

class SingletonLazy{

    private static SingletonLazy instance = null;

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

从上述代码中,可以看出实例的创建并非是类加载的时候创建了,而是等到真正第一次使用的时候,判断条件:如果instance为null,这个时候才去创建实例。也就是说当不需要使用的时候,就不会创建实例。

这也就是所谓的懒汉模式,虽说是“懒”,但从效率上来看,需要使用的时候才创建,这样的效率是更高的。

问题一 

在懒汉模式中,既涉及到 “读操作”,也涉及到 “写操作”,因此是可能存在线程安全问题的。

通过前面的讲解,也可以发现程序实际上是通过多条指令来执行的,所以在懒汉模式中的getInstance方法中,先大致整体分为如下指令:

load:从内存中读取instance的值;

cmp:对 instance的值与 null 进行比较;

若条件满足,则进行new操作;

save:将new的值赋给instance;

以一种线程不安全的例子来讲解: 

 从中我们就可以看出,这里的线程安全问题,本质上就是读,比较和写操作不是原子的,这就导致了线程t2 读到的值可能是线程t1 还没来得及写的。这也就是脏读问题。

解决办法也就是对这三个操作进行加锁。 

class SingletonLazy{

        private static SingletonLazy instance = null;

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

 这时候 t2线程load得到的结果就是 线程t1 修改后的结果了,也就不再是null值了,因此不再创建新对象,而是返回现有对象。

问题二 

此时的代码,在每次 getInstance 操作的时候都会进行加锁,而加锁操作是有一定开销的;

而实际上,这里的加锁操作只需要针对在 new 出对象之前,才是有意义的。一旦 new 完对象了,后续调用 getInstance ,此时 instance 的值一定是非空的,所以加锁操作是没有必要的,所以可以做出如下优化:如果对象还没创建,就加锁;如果对象已经创建过了,就不用加锁;

 class SingletonLazy{

        private static SingletonLazy instance = null;

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

 加锁 if 条件之后,负责判定是否需要加锁,此时就不再是无脑加锁了,也就提高了运行效率。

问题三

此时的代码依旧存在线程安全问题,假设有很多线程,都去进行 getInstance,这个时候,是有可能会被优化的,正如我们之前所讲的情况:只有第一次读才是真正读了内存,后续的读都是读取寄存器/cache,这也就涉及到内存可见性问题了。

除此之外,指令重排序也是会导致线程安全问题的。

instance = new SingletonLazy();

这条语句会有三条指令:

1.申请内存空间;

2.调用构造方法,把这个内存空间初始化成一个合理的对象;

3.把内存空间的地址赋值给 instance 引用;

正常情况下,是按照123的顺序来执行的,但编译器为了提高程序效率,可能就会调整执行顺序,在多线程的环境下,就会可能出现线程安全问题。(单线程环境下没有关系)

例如:假设线程t1 按照 132的顺序来执行,t1线程执行完1 3之后,在执行2 之前,被切出CPU了,这时候线程t2 进行执行,就会发现instance 的引用非空,那么线程t2 就会直接返回instance引用,并且可能会尝试使用 引用的属性。但由于线程t2 此时拿到的是非法的对象,也就是没构造完成的不完整的对象,再去使用的话就会出现线程安全问题。

因此针对这个问题,就需要使用volatile来修饰,volatile 解决两个问题:

1.内存可见性;2.指令重排序;

因此最终优化后的代码为:

//经典面试题,解决多线程安全问题!!!
// 懒汉模式的 单例模式 的实现
    class SingletonLazy{
    private volatile static SingletonLazy instance = null;      //volatile来解决内存可见性,指令重排序

    public static SingletonLazy getInstance(){
        if(instance == null){                           //不再是无脑加锁,而是满足判断是否为空再加锁
            synchronized (SingletonLazy.class) {                //对类对象进行加锁
                if ( instance == null){
                    instance = new SingletonLazy();
                }
            }
        }

        return instance;
    }

    private SingletonLazy(){}
}
public class ThreadDemo20 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }

}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

PlLI-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值