单例模式是一种大家比较熟悉的模式,就不多做介绍了。下面说一下多线程中遇到的问题吧
比如有一个计数器,每次输出都会自增1,常规单例模式的写法如下:
public class Counter {
private static int sum;
private static Counter counter;
private Counter() {
sum = 0;
}
public static Counter getInstance() {
if (counter == null) {
counter = new Counter();
}
System.out.println("sum:"+sum++);
return counter;
}
}
创建100个线程输出sum,测试代码如下:
public class Test {
public static void main(String[] args){
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
Counter.getInstance();
}
}).start();
}
}
}
我们来看一下单例模式的定义:确保一个类只有一个实例,并提供一个全局访问点。
根据这个定义来看,输出的结果应该为按顺序为0到99,但是实际输出如下:
sum:0
sum:2
sum:1
sum:0
sum:3
sum:4
sum:5
sum:8
sum:7
sum:6
sum:10
sum:9
………………
后面就省略了,每次运行的结果应该也不一样。
后来思考了一下原因,可能是每个线程得到的对象不是同一个对象,于是在getInstance方法加入线程锁。
public static synchronized Counter getInstance() {
if (counter == null) {
counter = new Counter();
}
System.out.println("sum:"+sum++);
return counter;
}
这样看来问题似乎解决了,但是似乎这样的做法会性能很不好。
如果应用程序总是创建并且使用单件实例,或者在创建和运行时不是很大的运算,可以这样修改代码:
public class Counter {
private static int sum;
private static Counter counter=new Counter();
private Counter() {
sum = 0;
}
public static synchronized Counter getInstance() {
System.out.println("sum:"+sum++);
return counter;
}
}
这样的写法,JVM虚拟机在加载这个类时会马上创建这个类唯一的实例,保证任何线程访问前都会先创建这个实例,使用的也都是这个实例。
还有一种写法就是利用双重检查枷锁,在getInstance中减少使用同步。会先检查实例是否已经被创建,如果没有被创建才会进行同步。写法如下:
public class Counter {
private static int sum;
private volatile static Counter counter;
private Counter() {
sum = 0;
}
public static synchronized Counter getInstance() {//检查实例,如果不存在,就进入同步区域
if (counter == null) {//只有第一次才彻底执行这些所有代码
synchronized (Counter.class) {
counter = new Counter();
}
}
System.out.println("sum:" + sum++);
return counter;
}
}
测试结果也是正常的0到99
volatile不适用于jdk1.4以前。
总结:
不可以把单例类当成父类设计出子类。因为继承单例类的构造器是私有的,不能用似有构造器来扩展类,所以必须把构造器变成共有的,但是变成共有的之后,就不能保证只有一个实例了。还有一个问题就是,单例的实现是利用静态变量,直接结成会导致所有派生类共同使用一个实例变量。