单例模式
什么是设计模式呢?
设计模式就好比棋手中的棋谱。在日常开发中,会会遇到很多常见的"问题场景",针对这些问题场景,大佬们就设计了一些固定套路,按照这些固定套路来实现代码或应对这些问题场景,也不会吃亏。这些固定的套路就是设计模式。
单例模式就是设计模式中的一个非常经典的一个设计模式,也是校招中最容易考到的设计模式。
单例模式可以保证某个类在程序中只有唯一 一个实例化对象,不会存在多个实例化对象。
单例模式的实现方式有很多种,最常见的有**"饿汉模式"** 和**"懒汉模式"**这两种。
饿汉模式
饿汉模式就是讲究一个迫切,在饿汉模式中,要求在加载类的同时,创建实例。
实现方式:在该类中,使用static修饰类成员对象,并将该类的构造方法用private修饰,使其私有化,并提供一个方法来获得对象。
实现代码:
java
class Singleton{
private static Singleton instance=new Singleton();
public Singleton getInstance(){
return instance;
}
private Singleton(){}
}
懒汉模式
懒就是尽量晚的创建实例,甚至不创建实例,也就是延迟创建实例,这样就方便我们根据实际需求来创建合适的实例。
实现方式:在该类中,先让static修饰的类成员置为null,然后通过方法来创建类的实例和获取实例。
代码实现
java
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成功实例化之后,后面线程涉及的操作就不涉及线程安全问题了,所以我们就可以让后面执行的线程跳过加锁的操作。
java
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条
申请内存空间
在空间中构造化对象,也就是初始化对象
3.将内存空间的"首地址"赋值给引用变量
正常的执行顺序是1->2->3,但是由于指令重排序的问题会出现1->3->2这样的执行顺序,也就是instance不为null了,但是还没初始化里面的内容,此时就会导致其他线程拿着一个未初始化的对象进行其他操作,这样就会导致线程安全问题。
针对指令重排序的问题,我们也是通过volatile来修饰类成员来解决。
volatile关键字的作用
1.确保程序成内存中读取数据,避免内存可见性问题
2.确保程序对变量的读取和修改不会出现指令重排序的问题。
懒汉模式的完整版代码
java
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(){}
}