单例模式(关于线程安全)

目录

单例模式

饿汉式

懒汉式

资源的加载和性能

多线程环境单例模式

饿汉式

懒汉式


单例模式

单例模式我们最常用的写法就是饿汉式和懒汉式。

那么饿汉式和懒汉式的区别是什么呢?

我们可以看一个简单的例子来了解饿汉式和懒汉式的区别:

首先我们需要知道什么是单例模式:单例单例,顾名思义就是在程序运行的过程中始终只能有一个对象实例,不能再出现第二个对象实例。

饿汉式和懒汉式的区别 举个栗子:

比如说我们需要读取计算机中的一个10个G的文件,饿汉式就是直接把这10个G的文件全部读取到内存中,然后进行显示。这样的操作很明显不是很好,因为我们不知道内存到底够不够,而且10个G的文件打开肯定也要卡半天。相比之下,懒汉式就很聪明了。懒汉式就是不一次性全部打开这个文件,而是只把文件的一小部分显示在当前用户的界面上,当用户翻页的时候,在去读取其他的文件进行显示,而不是一次性全部读取完成。懒汉式就能很快的打开这个文件。

对于单例类,我们需要注意一下几点:

1:单例类只能有一个实例

2:单例类必须自己创建自己的唯一实例

3:单例类必须给其他对象提供这一实例

4:构造方法必须私有化(防止外部直接new)

饿汉式

class Singleton{
    private static Singleton singleton = new Singleton();   //static 修饰,属于类对象 只有一份
​
    public static Singleton getSingleton() {
        return singleton;
    }
    //通过构造器私有化 来限制单例
    private Singleton() {}
}
​
public class threadDemo17 {
    //单例模式
    public static void main(String[] args) {
        //s1和s2都是同一份实例
        Singleton s1 = Singleton.getSingleton();
        Singleton s2 = Singleton.getSingleton();
​
        //报错   构造器私有化
        //Singleton s3 = new Singleton();
    }
}

我们可以看到饿汉式不管你需不需要这个实例,他都是第一时间先把这个实例创建好。等你需要的时候再去获取。此时我们可以看到,获取到的实例都是同一个实例。

懒汉式

class SingletonLazy {
    //懒汉式
    public static SingletonLazy singletonLazy = null;
​
    public static SingletonLazy getSingletonLazy() {
      if(singletonLazy == null) {
          return singletonLazy = new SingletonLazy();
      }
      return singletonLazy;
    }
​
    private SingletonLazy() {} //构造器私有化
}
public class threadDemo18 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getSingletonLazy();
        SingletonLazy s2 = SingletonLazy.getSingletonLazy();
        System.out.println(s1 == s2);
    }
}

上述代码我们可以看到,代码层面饿汉式和懒汉式的区别,饿汉式是在类加载的时候直接把实例创建好,而懒汉式则是在你真正需要这个实例的时候再去给你创建这个实例。

资源的加载和性能

饿汉式在类加载的时候已经创建出来一个静态的实例出来了,这个实例也会占据一定的内存,所以在第一次加载这个类的时候可能会速度较低,但是在第一次调用返回实例的时候,速度会很快,因为对象实例已经创建完成了,只需要返回即可。

懒汉式则是在类进行加载的时候不创建静态实例,也不会占据更多的内存空间,但是在第一次调用返回实例的时候,需要进行对象的实例创建,此时就会导致速度降低。

多线程环境单例模式

上述都是在单线程下的场景。

那么如果在多线程场景下,应该如何使用?

首先我们需要明白线程不安全的因素都有什么?

1:抢占式执行

2:多个线程修改同一个变量

3:修改操作不是原子的(不可分割的最小单位)

4:内存可见性

5:指令重排序

上述就是造成线程不安全的主要因素。

饿汉式

根据上述因素我们可以看出饿汉式在多线程环境下是天然安全的,因为饿汉式不涉及到修改操作,只是读取操作。

懒汉式

懒汉式则是涉及到了修改的操作,根据下述代码我们也可以看出因为在类加载的时候singletonLazy 实例是为空的,如果此时有对个线程需要获取这个单例类的实例时,就会出现new操作进行了多次的情况。假如此时有2个线程t1和t2在并发执行,当t1执行到if判断的时候,条件为真,在进行new操作之前,此时CPU突然调度t2线程开始执行,也进行if判断,条件也为真,也准备进行new操作,此时就会返回两个不同的实例了,这也就违背了单例模式的初衷。此时也就无法保证创建的对象是唯一的了。

 public static SingletonLazy getSingletonLazy() {
      if(singletonLazy == null) {
          return singletonLazy = new SingletonLazy();
      }
      return singletonLazy;
    }

那么如何解决呢?

我们就得从造成线程不安全的原因入手:

我们需要保证整个if判断和new操作是原子的,并且还有保证指令重排序问题。

我们可以采用加锁和禁止指令重排序

解决单例模式线程安全的问题:

  • 保证if和new是原子性的

  • 使用双重if减少不必要的加锁操作

  • 使用volatile关键字禁止指令重排序

class SingletonLazy {
    //懒汉式
   private static SingletonLazy singletonLazy = null; //禁止指令重排序
    public static SingletonLazy getSingletonLazy() {
        if(singletonLazy == null) {  //这个条件用来判断是否要加锁,如果对象已经有了,则在不加锁,此时本身就是线程安全的
            synchronized (SingletonLazy.class) {    //保证if() 判定和new是一个原子操作
                if(singletonLazy == null) {
                    singletonLazy = new  SingletonLazy();
                }
            }
        }
        return singletonLazy;
    }

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

可以看出我们并没有对整个方法进行加锁,因为加锁就意味着效率就要降低,锁的粒度就更粗。

我们可以看看下面的核心代码,首先我们使用双重if进行判断,第一个if就是用来判断是否需要进行加锁操作,如果对象已经存在,则不进行加锁,直接返回,此时线程天然安全的。如果此时对象还没有创建出来,就需要进行加锁操作,因为这个方法是static修饰的静态方法,那么此时的加锁对象就应该是类对象,加锁之后,我们的第二个if则是判断此时对象是否存在,如果不存在就进行new操作,但是在进行new操作的同时,可能就有bug。

假如此时有两个线程t1和t2在并发执行,当t1线程进行第一个if后,加锁成功了,成功之后在进行new操作的过程中,由于编译器优化的指令重排序,此时已经有创建好了这个实例,但是这个实例里面并没有真正的数据,此时CPU切换到t2线程上,t2线程在进行第一个if判断时候,发现条件为假,所以直接返回,但是返回的这个实例时不合适的,因为指令重排序的问题,这就导致t2线程返回的就是一个空壳子。

private static SingletonLazy singletonLazy = null; //禁止指令重排序
    public static SingletonLazy getSingletonLazy() {
        if(singletonLazy == null) {  
//这个条件用来判断是否要加锁,如果对象已经有了,则在不加锁,此时本身就是线程安全的
            synchronized (SingletonLazy.class) {    //保证if() 判定和new是一个原子操作
                if(singletonLazy == null) {
                    singletonLazy = new  SingletonLazy();
                 
                }
            }
        }
        return singletonLazy;
    }

解决上述问题也非常简单粗暴,我们直接把singletonLazy这个引用用volatile修饰就解决了指令重排序的问题。

下面是最终代码:

class SingletonLazy {
    //懒汉式
    volatile private static SingletonLazy singletonLazy = null; //禁止指令重排序
    public static SingletonLazy getSingletonLazy() {
        if(singletonLazy == null) {  //这个条件用来判断是否要加锁,如果对象已经有了,则在不加锁,此时本身就是线程安全的
            synchronized (SingletonLazy.class) {    //保证if() 判定和new是一个原子操作
                if(singletonLazy == null) {
                    singletonLazy = new  SingletonLazy();
                    //有可能会出现指令重排序,所有要把singletonLazy引用用volatile修饰
                }
            }
        }
        return singletonLazy;
    }

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

现在就保证了懒汉式在多线程下的安全问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值