首页 > 其他分享 >饿汉模式,懒汉模式线程安全问题

饿汉模式,懒汉模式线程安全问题

时间:2022-11-21 13:11:42浏览次数:66  
标签:Singleton 模式 instance 线程 饿汉 static 懒汉

饿汉模式:
上来不管三七二十一直接创建对象再说。

饿汉模式创建方式:

1.先创建一个私有的构造函数(防止其它地方直接实例化)

2.定义私有变量

3.提供公共的获取实例的方法

public class Single {
    //饿汉模式
    //先创建一个表示单例的类
    static class Singleton {
        //创建一个无法在该类外部实例化的私有构造方法
        private Singleton(){};
        //创建一个static成员,表示Singleton的唯一实例
        private static Singleton instance = new Singleton();
        //获取实例的唯一方法
        public static Singleton getInstance() {
            return instance;
        }
    }
}

 


懒汉模式:
当程序启动之后并不会进行初始化,在什么时候调用什么时候初始化。

懒汉模式:

public class Single1 {
    //懒汉模式
    static class Singleton {
        private Singleton() {}
        private static Singleton instance = null;
        public static Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }
}

 

类加载的时候,没有立刻实例化,第一次调用getInstance()的时候,才真的实例化.

如果要是代码一整场都没有调用getInstance,此时实例化的过程也就被省略掉了,

我们一般成懒汉模式为“懒加载”或者“延时加载”,“懒汉模式"比“饿汉模式"效率更高,有很大的可能是“实例用不到",此时就节省了实例化的开销。

线程安全问题:
了解了懒汉模式和饿汉模式的代码之后,如何分辨懒汉模式和饿汉模式是否线程安全呢,首先确认什么情况会导致线程不安全:

1.线程的调度是抢占式执行.

2.修改操作不是原子的

3.多个线程同时修改同一个变量

4.内存可见性

5.指令重排序

饿汉模式,懒汉模式线程安全吗?
了解了线程安全之后,对于饿汉模式来说,多线程同时调用getInstance(),由于getInstance()里只做了一件事:读取instance实例的地址,也就是多个线程在同时读取同一个变量,并没有构成多个线程同时修改同一个变量这一情况,所以说饿汉模式是线程安全的。

而对于懒汉模式来说,多线程调用getInstance(),getInstance()做了四件事情~

1.读取 instance 的内容

2.判断 instance 是否为 null

3.如果 instance 为null,就 new实例 (这就会修改 intance 的值)

4.返回实例的地址

由于懒汉模式造成了多个线程同时修改同一个变量这一情况,所以说懒汉模式是线程不安全的。

在instance实例化之前调用getInstance(),存在线程安全问题,如果已经把实例创建好了,后面再去并发调用getInstance()就是线程安全的了,而此时也就变成了饿汉模式。

为了解决“多个线程同时修改同一个变量”造成线程不安全的问题,采用加锁的解决方案,这里使用sychronized来解决线程不安全的问题。

public class Single2 {
    //懒汉模式
    static class Singleton {
        private Singleton() {}
        privatestatic Singleton instance = null;
        public static Singleton getInstance() {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
            return instance;
        }
    }
 
}

 

然而在上面的代码中,即使instance已经实例化了,但是每次调用getInstance()还是会涉及加锁解锁,实际上此时已经不需要了,所以要实现在instance实例化之前调用的时候加锁,之后不加锁,就引出了双重检验锁版本。

public class Single2 {
    //懒汉模式
    static class Singleton {
        private Singleton() {}
        private static Singleton instance = null;
        public static Singleton getInstance() {
            //双重检验锁
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
}

 

编译器优化:

只有第一次读操作从内存中读取数据同时存放在CPU的寄存器中,因为从寄存器中读取数据速度远大于从内存中读取,所以后续的读操作就直接从寄存器中读取数据。

此时我们已经基本完成懒汉模式的设计,但是懒汉模式在多线程情况下由于编译器优化还会出现一种特殊情况,某个线程可能会进行多次读操作:

线程1第一次读取instance为空,而线程2未释放锁时,加锁失败,与此同时其他线程也读取到instance为空,于是实例化instance后线程2释放锁,此时线程1加锁成功,但读取instance仍然为null。此时的线程2进行了多次读操作,然而由于编译器优化,导致线程2没有读到最新的数据,即实例化的instance。

此时需要使用volatile来解决这种特殊情况带来的问题。

volatile的作用:保持内存可见性,禁止编译器进行某种场景的优化(一个线程在读,一个线程在写,修改对于读线程来说可能没有生效)

懒汉模式最终版本:

public class Single2 {
    //懒汉模式
    static class Singleton {
        private Singleton() {}
        //volatile 避免内存可见性引出的问题
        private volatile static Singleton instance = null;
        public static Singleton getInstance() {
            //双重检验锁
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
}

 

总结:
为了保证懒汉模式线程安全,涉及到三个要点:

1.加锁保证线程安全

2.双重if保证效率

3. volatile避免内存可见性引来的问题.

内容转自 (33条消息) 面试官问我:饿汉模式,懒汉模式线程安全吗?我用这个直接绝杀!__RailGun_的博客-CSDN博客

标签:Singleton,模式,instance,线程,饿汉,static,懒汉
From: https://www.cnblogs.com/anzf/p/16911113.html

相关文章

  • 设计模式--创建型模式
    设计模式--创建型模式创建型模式,共五种:工厂方法模式(一个工厂类ReturnNew子类)、抽象工厂模式(一个厂一个子类)、单例模式(恶汉,懒汉)、建造者模式(组合)、原型模式(Cloneable浅......
  • 设计模式--结构型模式
    设计模式--结构型模式结构型模式,共七种:适配器模式(新接口用老实现类)、装饰器模式(同代理,增强)、代理模式(方法前后)、外观模式(计算机包括…)、桥接模式(中间表)、组合模式(树)、......
  • 设计模式--行为型模式
    设计模式--行为型模式行为型模式,共十一种:策略模式(Calculator子类)、模板方法模式(大象装冰箱)、观察者模式(观察者列表)、迭代子模式、责任链模式、命令模式(一层接一层)、备忘......
  • 命令模式
    计算器例子:publicclassCommand:ICommand{privateQueue<decimal>queue=newQueue<decimal>();publicdecimal[]GetResults=>queue.ToA......
  • 线程的私有空间
    线程内部的全局变量如果需要在一个线程内部的各个函数调用都能访问、但其它线程不能访问的变量,这就需要新的机制来实现,我们称之为Staticmemorylocaltoathread(线程局......
  • 【Java】JDK5.0新增的创建多线程的方式:实现Callable接口,使用线程池
    1.实现Callable接口方式和实现Runnable接口相比call()可以有返回值。call()可以抛出异常,被外面的操作捕获,获取异常信息。Callable是支持泛型的。实现Callable接口......
  • 114:设计模式_工厂模式实现
       设计模式是面向对象语言特有的内容,是我们在面临某一类问题时候固定的做法,设计模式有很多种,比较流行的是:GOF(GoupOfFour)23种设计模式。当然,我们没有必要全部学习......
  • 【设计模式】原型模式:猴头,我叫你一声你敢答应吗?
    1原型模式1.1概述原型模式是一种对象创建型模式,用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。原型模式允许一个对象再创建另外一个可定制的对象,无须知......
  • 用最少的代码模拟gRPC四种消息交换模式
    我们知道,建立在HTTP2/3之上的gRPC具有四种基本的通信模式或者消息交换模式(MEP:MessageExchangePattern),即Unary、ServerStream、ClientStream和BidirectionalStream。......
  • Vim实用技巧(4)——可视模式
    可视模式可视模式技巧20深入理解可视模式技巧20深入理解可视模式可视模式允许选中一个文本区域并在其上操作。h,j,k,l仍可当作光标键使用,f{char}配合,,;,查找......