[TOC]java
单例模式,是一种比较简单的设计模式,也是属于建立型模式(提供一种建立对象的模式或者方式)。
要点:设计模式
3.单例模式能够分为两种:懒汉模式(在第一次使用类的时候才建立,能够理解为类加载的时候特别懒,要用的时候才去获取,要是没有就建立,因为是单例,因此只有第一次使用的时候没有,建立后就能够一直用同一个对象),饿汉模式(在类加载的时候就已经建立,能够理解为饿汉已经饿得饥渴难耐,确定先把资源牢牢拽在本身手中,因此在类加载的时候就会先建立实例)安全
关键字:多线程
- 单例:
singleton
- 实例:
instance
- 同步:
synchronized
第一种single
是public
,能够直接经过Singleton
类名来访问。并发
public class Singleton { // 私有化构造方法,以防止外界使用该构造方法建立新的实例 private Singleton(){ } // 默认是public,访问能够直接经过Singleton.instance来访问 static Singleton instance = new Singleton(); }
第二种是用private
修饰singleton
,那么就须要提供static
方法来访问。函数
public class Singleton { private Singleton(){ } // 使用private修饰,那么就须要提供get方法供外界访问 private static Singleton instance = new Singleton(); // static将方法归类全部,直接经过类名来访问 public static Singleton getInstance(){ return instance;. } }
饿汉模式,这样的写法是没有问题的,不会有线程安全问题(类的static
成员建立的时候默认是上锁的,不会同时被多个线程获取到),可是是有缺点的,由于instance
的初始化是在类加载的时候就在进行的,因此类加载是由ClassLoader
来实现的,那么初始化得比较早好处是后来直接能够用,坏处也就是浪费了资源,要是只是个别类使用这样的方法,依赖的数据量比较少,那么这样的方法也是一种比较好的单例方法。
在单例模式中通常是调用getInstance()
方法来触发类装载,以上的两种饿汉模式显然没有实现lazyload
(我的理解是用的时候才触发类加载)
因此下面有一种饿汉模式的改进版,利用内部类实现懒加载。
这种方式Singleton类
被加载了,可是instance
也不必定被初始化,要等到SingletonHolder
被主动使用的时候,也就是显式调用getInstance()
方法的时候,才会显式的装载SingletonHolder
类,从而实例化instance
。这种方法使用类装载器保证了只有一个线程可以初始化instance
,那么也就保证了单例,而且实现了懒加载。学习
值得注意的是:静态内部类虽然保证了单例在多线程并发下的线程安全性,可是在遇到序列化对象时,默认的方式运行获得的结果就是多例的。优化
public class Singleton { private Singleton(){ } //内部类 private static class SingletonHolder{ private static final Singleton instance = new Singleton(); } //对外提供的不容许重写的获取方法 public static final Singleton getInstance(){ return SingletonHolder.instance; } }
最基础的代码(线程不安全):线程
public class Singleton { private static Singleton instance = null; private Singleton(){ } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
这种写法,是在每次获取实例instance
的时候进行判断,若是没有那么就会new
一个出来,不然就直接返回以前已经存在的instance
。可是这样的写法不是线程安全的,当有多个线程都执行getInstance()
方法的时候,都判断是否等于null的时候,就会各自建立新的实例,这样就不能保证单例了。因此咱们就会想到同步锁,使用synchronized关键字:
加同步锁的代码(线程安全,效率不高)设计
public class Singleton { private static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { synchronized(Singleton.class){ if (instance == null) instance = new Singleton(); } return instance; } }
这样的话,getInstance()
方法就会被锁上,当有两个线程同时访问这个方法的时候,总会有一个线程先得到了同步锁,那么这个线程就能够执行下去,而另外一个线程就必须等待,等待第一个线程执行完getInstance()
方法以后,才能够执行。这段代码是线程安全的,可是效率不高,由于假若有不少线程,那么就必须让全部的都等待正在访问的线程,这样就会大大下降了效率。那么咱们有一种思路就是,将锁出现等待的几率再下降,也就是咱们所说的双重校验锁(双检锁)。
public class Singleton { private static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null){ synchronized(Singleton.class){ if (instance == null) instance = new Singleton(); } } return instance; } }
1.第一个if判断,是为了下降锁的出现几率,前一段代码,只要执行到同一个方法都会触发锁,而这里只有singleton
为空的时候才会触发,第一个进入的线程会建立对象,等其余线程再进入时对象已建立就不会继续建立,若是对整个方法同步,全部获取单例的线程都要排队,效率就会下降。
2.第二个if判断是和以前的代码起同样的做用。
上面的代码看起来已经像是没有问题了,事实上,还有有很小的几率出现问题,那么咱们先来了解:原子操做,指令重排。
- 原子操做,能够理解为不可分割的操做,就是它已经小到不能够再切分为多个操做进行,那么在计算机中要么它彻底执行了,要么它彻底没有执行,它不会存在执行到中间状态,能够理解为没有中间状态。好比:赋值语句就是一个原子操做:
n = 1; //这是一个原子操做
假设n的值之前是0,那么这个操做的背后就是要么执行成功n等于1,要么没有执行成功n等于0,不会存在中间状态,就算是并发的过程当中也是同样的。
下面看一句不是原子操做的代码:
int n =1; //不是原子操做
缘由:这个语句中能够拆分为两个操做,1.声明变量n,2.给变量赋值为1,从中咱们能够看出有一种状态是n被声明后可是没有来得及赋值的状态,这样的状况,在并发中,若是多个线程同时使用n,那么就会可能致使不稳定的结果。
所谓指令重排,就是计算机会对咱们代码进行优化,优化的过程当中会在不影响最后结果的前提下,调整原子操做的顺序。好比下面的代码:
int a ; // 语句1 a = 1 ; // 语句2 int b = 2 ; // 语句3 int c = a + b ; // 语句4
正常的状况,执行顺序应该是1234,可是实际有多是3124,或者1324,这是由于语句3和4都没有原子性问题,那么就有可能被拆分红原子操做,而后重排.
原子操做以及指令重排的基本了解到这里结束,看回咱们的代码:
主要是
instance = new Singleton()
,根据咱们所说的,这个语句不是原子操做,那么就会被拆分,事实上JVM(java虚拟机)对这个语句作的操做:
在一个线程里面是没有问题的,那么在多个线程中,JVM作了指令重排的优化就有可能致使问题,由于第二步和第三步的顺序是不可以保证的,最终的执行顺序多是 1-2-3 也多是 1-3-2。若是是后者,则在 3 执行完毕、2 未执行以前,被线程二抢占了,这时 instance
已是非 null
了(但却没有初始化),因此线程二会直接返回instance
,而后使用,就会报空指针。
从更上一层来讲,有一个线程是instance已经不为null可是仍没有完成初始化中间状态,这个时候有一个线程刚恰好执行到第一个if(instance==null
),这里获得的instance
已经不是null
,而后他直接拿来用了,就会出现错误。
对于这个问题,咱们使用的方案是加上volatile
关键字。
public class Singleton { private static volatile Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null){ synchronized(Singleton.class){ if (instance == null) instance = new Singleton(); } } return instance; } }
volatile
的做用:禁止指令重排,把instance
声明为volatile
以后,这样,在它的赋值完成以前,就不会调用读操做。也就是在一个线程没有完全完成instance = new Singleton()
;以前,其余线程不可以去调用读操做。
- 上面的方法实现单例都是基于没有复杂序列化和反射的时候,不然仍是有可能有问题的,还有最后一种方法是使用枚举来实现单例,这个能够说的比较理想化的单例模式,自动支持序列化机制,绝对防止屡次实例化。
public enum Singleton { INSTANCE; public void doSomething() { } }
以上最推荐枚举方式,固然如今计算机的资源仍是比较足够的,饿汉方式也是不错的,其中懒汉模式下,若是涉及多线程的问题,也须要注意写法。
最后提醒一下,volatile
关键字,只禁止指令重排序,保证可见性(一个线程修改了变量,对任何其余线程来讲都是当即可见的,由于会当即同步到主内存),可是不保证原子性。
【做者简介】:
秦怀,公众号【秦怀杂货店】做者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界但愿一切都很快,更快,可是我但愿本身能走好每一步,写好每一篇文章,期待和大家一块儿交流。
此文章仅表明本身(本菜鸟)学习积累记录,或者学习笔记,若有侵权,请联系做者核实删除。人无完人,文章也同样,文笔稚嫩,在下不才,勿喷,若是有错误之处,还望指出,感激涕零~