Bootstrap

多线程案例---单例模式

单例模式

什么是设计模式呢?

设计模式就好比棋手中的棋谱。在日常开发中,会会遇到很多常见的“问题场景”,针对这些问题场景,大佬们就设计了一些固定套路,按照这些固定套路来实现代码或应对这些问题场景,也不会吃亏。这些固定的套路就是设计模式

单例模式就是设计模式中的一个非常经典的一个设计模式,也是校招中最容易考到的设计模式。

单例模式可以保证某个类在程序中只有唯一 一个实例化对象,不会存在多个实例化对象。

单例模式的实现方式有很多种,最常见的有“饿汉模式”“懒汉模式”这两种。

 饿汉模式

饿汉模式就是讲究一个迫切,在饿汉模式中,要求在加载类的同时,创建实例

实现方式:在该类中,使用static修饰类成员对象,并将该类的构造方法用private修饰,使其私有化,并提供一个方法来获得对象。

实现代码:

class Singleton{
    private static Singleton instance=new Singleton();
    
    public Singleton getInstance(){
        return instance;
    }
    
    private Singleton(){}
}

懒汉模式

 懒就是尽量晚的创建实例,甚至不创建实例,也就是延迟创建实例,这样就方便我们根据实际需求来创建合适的实例

实现方式:在该类中,先让static修饰的类成员置为null,然后通过方法来创建类的实例和获取实例。

代码实现

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

线程安全分析

 在多线程的情况下,饿汉模式和懒汉模式是否存在线程安全问题?

判断饿汉模式和懒汉模式是否存在线程安全问题,主要是分析这两种模式的getInstance()方法是否存在线程安全问题。

饿汉模式

由于懒汉模式中的getInstance()方法中只有一个return语句,这是一个读操作,所以不会涉及线程安全问题。

懒汉模式

由于懒汉模式中的getInstance()方法中涉及到修改操作,在多线程程序中,有可能产生线程安全问题。

1.原子性分析

如下图

 

 

如上图,在多线程情况下就会出现上图这种情况, 这样就会导致第一次new的对象被第二次new的对象给覆盖掉了,第一个线程new出来的对象就会被GC释放掉了。

这里假设我们我们new的过程中要将一个很大内存的数据加载到内存中,本来加载一份这样的数据就要花费很多时间,但由于上述问题的存在,可能就要加载两份的数据,结果第一份数据还被释放掉了,这样反而降低了程序的运行效率

这里产生线程安全的原因是条件判断和修改操作不是原子的,这时,我们就可以通过加锁,将判断操作和修改操作打包成原子的。

如下图

引入加锁后,后执行的线程执行到加锁的位置就会阻塞,等到前一个线程执行完毕释放锁时,此时,instance就不为null了,所以第二个线程就不会执行new操作了,这样就避免出现加载两份new出相同对象的情况了,提高了程序的效率。 

2.锁效率分析

 当我们加锁之后,就会引入一个新的问题。

当我们的instance已经实例化好之后,多个线程在继序执行代码的时候,为了判断instance是否已经实例化,就会多次的加锁去执行所里面的判断操作,多个线程持续的加锁和解锁就会出现阻塞,一旦阻塞,对于计算机来说,阻塞的时间间隔就是沧海桑田,这样影响程序的效率。

解决方法:

我们可以按需加锁,真正涉及到线程安全问题的时候我们在加锁,不涉及到线程安全问题的时候,我们就不加锁。

以上面的的的代码为例

我们真正涉及到线程安全问题的时候是第一次实例化instance的时候,当我们第一次实例化成功后,后面执行的线程就不必再去加锁,进去所里面执行实例化的操作了,也就是说,第一次instance成功实例化之后,后面线程涉及的操作就不涉及线程安全问题了,所以我们就可以让后面执行的线程跳过加锁的操作

class SingletonLazy{

    private static SingletonLazy instance=null;

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

    private SingletonLazy(){}
}

注意:

这里出现了两次if(instance==null)

syncronized里面的if(instance==null)是为了判断是否需要实例化对象,最外面的if(instance==null)是为了在多线程程序中,instance已经实例化好的情况下,其他线程继续执行该代码的时候,不需要再继续实例化,让其跳过加锁和解锁的操作,直接执行return语句,使程序不会阻塞,提高程序的运行效率。

3.内存可见性分析

上面代码在多线程情况下会不会出现内存可见性的问题呢?

如下图

由于编译器优化是一个非常复杂的过程,我们无法确定是否出现内存可见性问题,但是为了杜绝内存可见性的问题,我们还是要用volatile关键字来修饰类成员 。

 

4.指令重排序分析 

这里更关键的问题,是指令重排序问题。

指令重排序也是编译器优化的一种体现,编译器优化能保证在代码逻辑不变的情况下,会改变代码指令执行的先后顺序来提高代码的运行效率。

如上面代码中的instance=new SingletonLazy();这个语句就有可能触发指令重排序的问题。

这条语句执行的指令主要有3条

1. 申请内存空间

2. 在空间中构造化对象,也就是初始化对象

3.将内存空间的“首地址”赋值给引用变量

正常的执行顺序是1->2->3,但是由于指令重排序的问题会出现1->3->2这样的执行顺序,也就是instance不为null了,但是还没初始化里面的内容,此时就会导致其他线程拿着一个未初始化的对象进行其他操作,这样就会导致线程安全问题。 

针对指令重排序的问题,我们也是通过volatile来修饰类成员来解决。

volatile关键字的作用

1.确保程序成内存中读取数据,避免内存可见性问题

2.确保程序对变量的读取和修改不会出现指令重排序的问题。 

懒汉模式的完整版代码

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

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;