一、问题背景
单例,大家都应该清楚,面试时也经常被问到,大家也都会写。但就是这个最常规的东西,让我有了新的认识。
问题是这样的,我正准备做一个不同情况时的性能测试。Bean的生成方式就是单例,getInstance()。测试时需要测试并发下代码的正常执行,所以采用多线程方式来调用getInstance()。这时问题来了,发现getInstance()并非真正的单例,被初始化了多次,次数不固定。Why?
二、问题分析
经过研究发现问题是由于高并发导致的,有很大几率导致多个if(instance==null)的判断同时为true,引发重复初始化。
有问题的代码MySingleton(X)
package org.noahx.singleton;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Created with IntelliJ IDEA.
* User: noah
* Date: 4/28/13
* Time: 9:37 PM
* To change this template use File | Settings | File Templates.
*/
public class MySingleton {
private static MySingleton mySingleton;
/**
* 原子计数器
*/
private static AtomicInteger count=new AtomicInteger(0);
private MySingleton() {
//模拟长时间初始化
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count.incrementAndGet());
}
public static MySingleton getInstance() {
if (mySingleton == null) {
mySingleton = new MySingleton();
}
return mySingleton;
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int c = 0; c < 20; c++) {
executorService.execute(new TestRun());
}
executorService.shutdown();
try {
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (Exception e) {
e.printStackTrace();
}
}
public static class TestRun implements Runnable {
@Override
public void run() {
System.out.println(MySingleton.getInstance());
}
}
}
有问题的输出结果
(X)
1
org.noahx.singleton.MySingleton@35ab28fe
2
org.noahx.singleton.MySingleton@86e293a
3
org.noahx.singleton.MySingleton@7854a328
4
org.noahx.singleton.MySingleton@7ca3d4cf
5
org.noahx.singleton.MySingleton@67e8a1f6
6
org.noahx.singleton.MySingleton@59e152c5
7
org.noahx.singleton.MySingleton@5801319c
8
org.noahx.singleton.MySingleton@366025e7
org.noahx.singleton.MySingleton@366025e7
9
org.noahx.singleton.MySingleton@6037fb1e
org.noahx.singleton.MySingleton@6037fb1e
org.noahx.singleton.MySingleton@6037fb1e
org.noahx.singleton.MySingleton@6037fb1e
10
org.noahx.singleton.MySingleton@7b479feb
11
org.noahx.singleton.MySingleton@375212bc
12
org.noahx.singleton.MySingleton@6d4c1103
13
org.noahx.singleton.MySingleton@1cf11404
14
org.noahx.singleton.MySingleton@17592174
org.noahx.singleton.MySingleton@17592174
org.noahx.singleton.MySingleton@17592174
这里可以清楚的看到,被初始化的14次(每次运行不固定),而且类也有时是不同的实例。
所以这样的单例方式其实不是绝对意义上的单例。
三、问题修正(会有DCL问题,见三2)
加锁吧,但又不想因为锁而影响运行的效率。所以采用了ReentrantLock(http://my.oschina.net/noahxiao/blog/101558),并使用双次==null判断解决性能问题。主要对getInstance()的代码进行了修改。(详见注释)
修正后的代码MySafetySingleton(V?)
package org.noahx.singleton;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created with IntelliJ IDEA.
* User: noah
* Date: 4/28/13
* Time: 9:37 PM
* To change this template use File | Settings | File Templates.
*/
public class MySafetySingleton {
private static MySafetySingleton mySingleton;
/**
* 原子计数器
*/
private static AtomicInteger count = new AtomicInteger(0);
/**
* 锁
*/
private static ReentrantLock lock = new ReentrantLock();
private MySafetySingleton() {
//模拟长时间初始化
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count.incrementAndGet());
}
public static MySafetySingleton getInstance() {
if (mySingleton == null) { //为了不影响以后运行的速度(非第一次)首先判定是否为null
try {
lock.lock(); //先上锁,来保证下面这个代码不会同时被执行
if (mySingleton == null) { //第二次判断是否为null,这样可以放弃由于初始并发而导致多次实例的问题
mySingleton = new MySafetySingleton();
}
} finally {
lock.unlock();
}
}
return mySingleton;
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int c = 0; c < 20; c++) {
executorService.execute(new TestRun());
}
executorService.shutdown();
try {
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (Exception e) {
e.printStackTrace();
}
}
public static class TestRun implements Runnable {
@Override
public void run() {
System.out.println(MySafetySingleton.getInstance());
}
}
}
修正后的输出结果(V?)
1
org.noahx.singleton.MySafetySingleton@6cf84386
org.noahx.singleton.MySafetySingleton@6cf84386
org.noahx.singleton.MySafetySingleton@6cf84386
org.noahx.singleton.MySafetySingleton@6cf84386
org.noahx.singleton.MySafetySingleton@6cf84386
org.noahx.singleton.MySafetySingleton@6cf84386
org.noahx.singleton.MySafetySingleton@6cf84386
org.noahx.singleton.MySafetySingleton@6cf84386
org.noahx.singleton.MySafetySingleton@6cf84386
org.noahx.singleton.MySafetySingleton@6cf84386
org.noahx.singleton.MySafetySingleton@6cf84386
org.noahx.singleton.MySafetySingleton@6cf84386
org.noahx.singleton.MySafetySingleton@6cf84386
org.noahx.singleton.MySafetySingleton@6cf84386
org.noahx.singleton.MySafetySingleton@6cf84386
org.noahx.singleton.MySafetySingleton@6cf84386
org.noahx.singleton.MySafetySingleton@6cf84386
org.noahx.singleton.MySafetySingleton@6cf84386
org.noahx.singleton.MySafetySingleton@6cf84386
org.noahx.singleton.MySafetySingleton@6cf84386
三2、问题修正(内部类)
首先谢谢David Wu提供的内部类方式的代码,经过测试确实方便的解决了问题。对于性能没有做比较,但使用时的性能应该不会有问题。具体见InnerClass与getInstance。
修正后代码MySafety2Singleton(V)
package org.noahx.singleton;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Created with IntelliJ IDEA.
* User: noah
* Date: 4/28/13
* Time: 9:37 PM
* To change this template use File | Settings | File Templates.
*/
public class MySafety2Singleton {
private static MySafety2Singleton mySafety2Singleton;
/**
* 原子计数器
*/
private static AtomicInteger count = new AtomicInteger(0);
private MySafety2Singleton() {
//模拟长时间初始化
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count.incrementAndGet());
}
public static MySafety2Singleton getInstance() {
if (mySafety2Singleton == null) {
mySafety2Singleton = InnerClass.SINGLETON;
}
return mySafety2Singleton;
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int c = 0; c < 20; c++) {
executorService.execute(new TestRun());
}
executorService.shutdown();
try {
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (Exception e) {
e.printStackTrace();
}
}
private static class InnerClass {
private static final MySafety2Singleton SINGLETON = new MySafety2Singleton();
}
public static class TestRun implements Runnable {
@Override
public void run() {
System.out.println(MySafety2Singleton.getInstance());
}
}
}
修正后运行结果(V)
1
org.noahx.singleton.MySafety2Singleton@1f194a4e
org.noahx.singleton.MySafety2Singleton@1f194a4e
org.noahx.singleton.MySafety2Singleton@1f194a4e
org.noahx.singleton.MySafety2Singleton@1f194a4e
org.noahx.singleton.MySafety2Singleton@1f194a4e
org.noahx.singleton.MySafety2Singleton@1f194a4e
org.noahx.singleton.MySafety2Singleton@1f194a4e
org.noahx.singleton.MySafety2Singleton@1f194a4e
org.noahx.singleton.MySafety2Singleton@1f194a4e
org.noahx.singleton.MySafety2Singleton@1f194a4e
org.noahx.singleton.MySafety2Singleton@1f194a4e
org.noahx.singleton.MySafety2Singleton@1f194a4e
org.noahx.singleton.MySafety2Singleton@1f194a4e
org.noahx.singleton.MySafety2Singleton@1f194a4e
org.noahx.singleton.MySafety2Singleton@1f194a4e
org.noahx.singleton.MySafety2Singleton@1f194a4e
org.noahx.singleton.MySafety2Singleton@1f194a4e
org.noahx.singleton.MySafety2Singleton@1f194a4e
org.noahx.singleton.MySafety2Singleton@1f194a4e
org.noahx.singleton.MySafety2Singleton@1f194a4e
运行正常而且也是懒加载。
四、总结
看似简单的问题总是隐藏杀机,稍有不慎就会出现问题。
后文补充:
谢谢大家的关注,写这个例子时我只是按照我的思路做出了修正,主要为了解决多次初始化的问题。确实没有查阅DCL会导致的其它问题。也没有想到更复杂的情况,如其它文章中阐述的对单例中对成员调用时可能获取到默认值的情况。
但经过我的程序模拟也没有模拟出DCL导致get成员变量时获得默认值的问题,希望其他人可以写一篇更详细带有测试代码的的blog为大家分享。
谢谢!
后文补充2:
感谢David Wu提供的源码,已经加入了内部类方式,见三2。