在面试过程中,设计模式是热门话题,在刚学java的时候,我只知道单例模式的两种写法,懒汉式和饿汉式。今天看了大神的博客,受益匪浅,决定做个关于单例模式的总结!
一 . 懒汉式(最普通的实现方法)
public class SingletonOne {
private static SingletonOne instance = null;
private SingletonOne() {
}
public static SingletonOne getInstance() {
if (instance == null) { // 1
instance = new SingletonOne(); // 2
}
return instance;
}
}
这种懒汉式的单例模式是线程不安全的,比如在多线程的环境下,线程A进入到了1的位置,此时instance==null,会执行2,与此同时,线程B在同一时间也进入了到了1的位置,也会执行2,这样就会new出了两个不一样的实例,这就违背了单例模式的设计理念,那如何解决线程安全的问题呢?
方法一:同步方法:在getInstance()前面加上一个synchronized关键字
public class SingletonTwo {
private static SingletonTwo instance = null;
private SingletonTwo() {
}
public static synchronized SingletonTwo getInstance() {
if (instance == null) { // 1
instance = new SingletonTwo; // 2
}
return instance;
}
}
加上synchronized后确实实现了线程的互斥访问getInstance()方法。从而保证了线程安全。但是这样就完美了么?我们看。其实在典型实现里,会导致问题的只是当instance还没有被实例化的时候,多个线程访问1的代码才会导致问题。而当instance已经实例化完成后。每次调用getInstance(),其实都是直接返回的。即使是多个线程访问,也不会出问题。但给方法加上synchronized后。所有getInstance()的调用都要同步了。其实我们只是在第一次调用的时候要同步。而同步需要消耗性能。
方法二:双重检查加锁Double-checked locking。其实经过分析发现,我们只要保证 instance = new SingletonOne()是线程互斥访问的就可以保证线程安全了。那把同步方法加以改造,只用synchronized块包裹这一句。就得到了下面的代码:
方法二:
public class SingletonThree {
private static SingletonThree instance = null;
private SingletonThree () {
}
public static SingletonThree getInstance() {
if (instance == null) { // 1
synchronized(SingletonThree .class) {
instance = new SingletonThree ; // 2
}
}
return instance;
}
}
这个方法可行么?分析一下发现是不行的!
1、线程A和线程B同时进入//1的位置。这时instance是为空的。
2、线程A进入synchronized块,创建实例,线程B等待。
3、线程A返回,线程B继续进入synchronized块,创建实例。。。
4、这时已经有两个实例创建了。
为了解决这个问题。我们需要在//2的之前,再加上一次检查instance是否被实例化。(双重检查加锁)接下来,代码变成了这样:
方法三:
public class SingletonThree {
private static SingletonThree instance = null;
private SingletonThree () {
}
public static SingletonThree getInstance() {
if (instance == null) { // 1
synchronized(SingletonThree .class) {
if (instance == null) {
instance = new SingletonThree() ; // 2
}
}
}
return instance;
}
}
这样,当线程A返回,线程B进入synchronized块后,会先检查一下instance实例是否被创建,这时实例已经被线程A创建过了。所以线程B不会再创建实例,而是直接返回。貌似!到此为止,这个问题已经被我们完美的解决了。遗憾的是,事实完全不是这样!这个方法在单核和 多核的cpu下都不能保证很好的工作。导致这个方法失败的原因是当前java平台的内存模型。java平台内存模型中有一个叫“无序写”(out-of-order writes)的机制。正是这个机制导致了双重检查加锁,这个问题的关键在上面代码: instance = new SingletonThree(),这行其实做了两个事情:1、调用构造方法,创建了一个实例。2、把这个实例赋值给instance这个实例变量。可问题就是,这两步jvm是不保证顺序的。也就是说。可能在调用构造方法之前,instance已经被设置为非空了。下面我们看一下出问题的过程:
1、线程A进入getInstance()方法。
2、因为此时instance为空,所以线程A进入synchronized块。
3、线程A执行 instance new SingletonThree(); 把实例变量instance设置成了非空。(注意,实在调用构造方法之前。)
4、线程A退出,线程B进入。
5、线程B检查instance是否为空,此时不为空(第三步的时候被线程A设置成了非空)。线程B返回instance的引用。(问题出现了,这时instance的引用并不是SingletonThree的实例,因为没有调用构造方法。)
6、线程B退出,线程A进入。
7、线程A继续调用构造方法,完成instance的初始化,再返回。
好吧,继续努力,解决由“无序写”带来的问题。
public static SingletonThree getInstance() {
if (instance == null) {
synchronized (SingletonThree.class) { // 1
SingletonThree temp = instance; // 2
if (temp == null) {
synchronized (SingletonThree.class) { // 3
temp = new SingletonThree(); // 4
}
instance = temp; // 5
}
}
}
return instance;
}
解释一下执行步骤。
1、线程A进入getInstance()方法。
2、因为instance是空的 ,所以线程A进入位置//1的第一个synchronized块。
3、线程A执行位置//2的代码,把instance赋值给本地变量temp。instance为空,所以temp也为空。 <wbr>
4、因为temp为空,所以线程A进入位置//3的第二个synchronized块。
5、线程A执行位置//4的代码,把temp设置成非空,但还没有调用构造方法!
6、线程A阻塞,线程B进入getInstance()方法。
7、因为instance为空,所以线程B试图进入第一个synchronized块。但由于线程A已经在里面了。所以无法进入。线程B阻塞。
8、线程A激活,继续执行位置//4的代码。调用构造方法。生成实例。
9、将temp的实例引用赋值给instance。退出两个synchronized块。返回实例。
10、线程B激活,进入第一个synchronized块。
11、线程B执行位置//2的代码,把instance实例赋值给temp本地变量。
12、线程B判断本地变量temp不为空,所以跳过if块。返回instance实例。
好吧,问题终于解决了,线程安全了。但是我们的代码由[最初的3行代码变成了现在的一大坨~。于是又有了下面的方法。
预先初始化static变量。
public class SingletonFour{
private singletonFour{
}
private static final SingletonFour instance=new SingletonFour();
public static SingletonFour getInstance()(
return instance;
}
}
由于java的机制,static的成员变量只在类加载的时候初始化一次,且类加载是线程安全的。所以这个方法实现的单例是线程安全的。但是这个方法却牺牲了Lazy的特性。单例类加载的时候就实例化了。如注释所述:非懒加载,如果构造的单例很大,构造完又迟迟不使用,会导致资源浪费。
那到底有没有完美的办法?懒加载,线程安全,代码简单。
使用内部类:
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
当getInstance方法第一次被调用的时候,它第一次读取LazyHolder.instance,内部类LazyHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。
这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。
网友评论