单例类是最简单的一个OOP设计模式,然而单例模式并不那么简单,因此技术面试中面试官往往会问到它。
这里总结了关于java中单例模式的一系列问题。仅供总结和分享,请不要用来难为面试的娃们。
1. 1最简单的单例--饿汉单例
饿汉单例指的是在类加载时完成单例对象实例化。下面是一个简饿汉单例的示例:
package Singletons;
public class GreedySingleton{
//private static
private static GreedySingleton instance = new GreedySingleton();
//private
private GreedySingleton(){}
//static
public static GreedySingleton getInstance(){
return instance;
}
}
简单得没啥可说,只有一点需要注意:该单例的实现中
getInstance()方法可以并发访问,不需要再添加synchronized之类的同步
其实关于饿汉单例还有一个学习中无法想到的问题,这个问题只有真正使用时才可能遇到,具体见下一节。
1.2 饿汉单例也不简单
话不多说,直接上码:
public class GreedySingleton_{
//A:
private static GreedySingleton_ instance = new GreedySingleton_();
//B:
private static int[] wallet;
//C:
static{
wallet = new int [2];
wallet[0] = 1;
}
//D:
private GreedySingleton_(){
if(wallet == null){
wallet = new int[2];
}
wallet[1] = 2;
}
public static GreedySingleton_ getInstance(){
return instance;
}
public void openUrWallet(){
System.out.println("["+wallet[0]+","+wallet[1]+"]");
}
public static void main(String[] args){
GreedySingleton_.getInstance().openUrWallet();
}
}
输出结果:
[1,0]
你可能猜对了,也可能分析错了,这都不重要,重要的是代码真正的执行情况:
程序从main方法开始,
首先遇到类GreedySingleton_,于是加载该类:
类加载经过class加载、验证、解析、准备,然后进入初始化阶段,执行<clinit>方法:
clinit方法首先执行
private static GreedySingleton_ instance = new GreedySingleton_():
这里是一个new指令,于是先判断GreedySingleton_是否已经加载,由于类加载工作除了初始化过程外其他过程均已完成,判定该类已经加载
于是分配一段堆内存来存放GreedySingleton_实例对象,并执行<init>方法:
构造方法外没有非静态属性的初始化和赋值操作,所以这里<init>的任务就是执行构造方法
GreedySingleton_():
这是wallet还处于null状态,因此执行
wallet = new int[2];
wallet[1] = 2;
从<init>退出,将<init>中分配内存的引用赋给instance属性
继续执行<clinit>的后续代码:
wallet = new int [2];
wallet[0] = 1; 这时wallet被指向另一个数组对象,其内容为[1,0] (之所以第二个未赋值的wallet[1]==0,原因是数组也是一个类,类实例化时new指令能保证分配到的是一段全零内存)
以上是代码执行过程。
从以上分析可以看到,<init>方法在<clinit>方法执行过程中被执行,除非非常了解JVM规范,否则无法知道代码真正的含义。
正常来讲,应该先执行<clinit>方法,等类加载过程全部完成,才可以执行<init>方法,否则只会带来麻烦而不会达到任何好处。
为了保证始终<clinit>先于<init>,可以遵循以下几个原则:
0. 尽量避免自身类型的类属性
1. 尽量避免在<clinit>涵盖代码部分中实例化自身引用
2. 尽量将自身类型实例化操作置于<clinit>涵盖代码的最后部分
2. 复杂点的单例--懒汉单例
懒汉单例就是等首次使用时才创建对象实例:
package Singletons;
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton(){}
//同步是为了防止instance属性未实例化时同时来了n个线程,他们同时到达代码:if(instance==null)
//都看到instance未创建,于是每人创建了一个出来
public synchronized LazySingleton getInstance(){
if(instance==null)
instance = new LazySingleton();
return instance;
}
}
但是有一个很大的问题:每次访问都要进行同步操作,大部分时间都花在同步上了。解决办法就是引入Double-Check。
《Java与模式》一书提到Java语言无法实现Double-Check(原因是由于指令重排导致实例化完成与属性的引用赋值先后无法预测,导致有些线程得到未初始化完成的单例对象)。但是早在JDK1.5对volatile进行修复之后,这个问题就已经解决:
package Singletons;
public class LazySingleton_ {
//volatile
private volatile static LazySingleton_ instance = null;
private LazySingleton_(){}
public static LazySingleton_ getInstance(){
//Double-Check:
if(instance == null){
synchronized (LazySingleton_.class) {
if(instance == null)
instance = new LazySingleton_();
}
}
return instance;
}
}
volatile阻止了对instance的操作进程指令重排,同时保证了其可视性
3. 真正的单例--安全单例
前面的单例均通过构造方法私有化来保证"单"的要求。
所谓安全单例,指不能通过反射得到其第二个实例。
安全单例可以通过内嵌类、抽象类两种方式来实现。
其中,内嵌类的方式得到的同时也是懒汉单例,是一个延迟加载的安全的单例,所以最为推荐。
详见我的另一篇博客http://blog.csdn.net/pbooodq/article/details/49125355