设计模式之单例模式
次访问
最简单的一种单例模式
package com.ans;
public class SingletonTest {
public static void main(String[] args) {
Singleton instance1 = Singleton.getSingleton();
Singleton instance2 = Singleton.getSingleton();
}
}
class Singleton {
private static Singleton instance = null;
public static Singleton getSingleton() {
if (instance == null) {
instance = new Singleton();
} else {
System.out.println("已有单例");
}
return instance;
}
private Singleton() {
System.out.println("开始构建单例");
}
}
运行结果:
开始构建单例
已有单例
分析
Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。
但是这有一个致命的弱点:线程安全性。
在高并发环境下很可能出现多个Singleton实例。
改进1
public static synchronized Singleton getSingleton() {
if (instance == null) {
instance = new Singleton();
} else {
System.out.println("已有单例");
}
return instance;
}
这样,线程安全是安全了,但是由于要加锁,去锁,造成效率低下而且99%情况下不需要同步。
改进2
package com.ans;
public class SingletonTest {
public static void main(String[] args) {
Singleton instance1 = Singleton.getSingleton();
Singleton instance2 = Singleton.getSingleton();
}
}
class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getSingleton() {
return instance;
}
private Singleton() {
System.out.println("开始构建单例");
}
}
运行结果:
开始构建单例
分析:
这种方法基于类加载机制,避免了同步带来的问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。
什么是 Lazy loading?
在维基百科上有这样一段话:
Lazy loading is a design pattern commonly used in computer programming to defer initialization of an object until the point at which it is needed. It can contribute to efficiency in the program’s operation if properly and appropriately used. The opposite of lazy loading is eager loading.
简言之,就是什么时候需要,什么时候加载。
类似地,还有一种思路,就是把对象的构造放在静态块里面。
package com.ans;
public class SingletonTest {
public static void main(String[] args) {
Singleton instance1 = Singleton.getSingleton();
Singleton instance2 = Singleton.getSingleton();
}
}
class Singleton {
private static Singleton instance = null;
static {
instance = new Singleton();
}
public static Singleton getSingleton() {
return instance;
}
private Singleton() {
System.out.println("开始构建单例");
}
}
严格意义上来说,还是一种改进方式。
改进3
package com.ans;
public class SingletonTest {
public static void main(String[] args) {
Singleton instance1 = Singleton.getSingleton();
Singleton instance2 = Singleton.getSingleton();
}
}
class Singleton {
private static Singleton instance = null;
static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getSingleton() {
return SingletonHolder.INSTANCE;
}
private Singleton() {
System.out.println("开始构建单例");
}
}
这种方式同样利用了classloder的机制来保证初始化newInstance时只有一个线程,它跟前面的方式不同的是(很细微的差别):改进2 Singleton 类被装载了,那么 instance 就会被实例化,没有达到lazy loading效果。
而这种方式是Sinlgeton类被装载了,instance 不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getSingleton方法时,才会显式装载SingletonHolder类,从而实例化instance类。
此处需要补一下内部类的加载机制:
可以先看一段代码来进行测试
package com.ans;
public class TestInnerClassLoading {
public static void main(String[] args) {
Outer outer = new Outer();//此刻其内部类是否也会被加载?
System.out.println("===========分割线===========");
Outer.StaticInner.staticInnerMethod();//调用内部类的静态方法
}
}
class Outer {
static {
System.out.println("load outer class...");
}
//静态内部类
static class StaticInner {
static {
System.out.println("load static inner class...");
}
static void staticInnerMethod() {
System.out.println("static inner method...");
}
}
}
运行结果:
load outer class...
===========分割线===========
load static inner class...
static inner method...
可以看到,只有在调用内部类的 staticInnerMethod() 方法时才会初始化内部类。
结论:加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生
想象一下,如果实例化Instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在 Singleton类加载时就实例化,因为我不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,前面的改进2就显得很合理。
改进4
传说中的双重检查。
package com.ans;
public class SingletonTest {
public static void main(String[] args) {
Singleton instance1 = Singleton.getSingleton();
Singleton instance2 = Singleton.getSingleton();
}
}
class Singleton {
private static Singleton instance = null;
public static Singleton getSingleton() {
if(instance == null) { //先判断有没有初始化实例
synchronized(Singleton.class) {//对对象上锁
if(instance == null) {//再次判断有没有初始化
instance = new Singleton();
}
}
}
return instance;
}
private Singleton() {
System.out.println("开始构建单例");
}
}
测试
package com.ans;
public class SingletonTest {
public static void main(String[] args) {
Singleton instance1 = Singleton.getSingleton();
Singleton instance2 = Singleton.getSingleton();
instance1.setName("Example1");
instance2.setName("Example2");
instance1.printName();
instance2.printName();
if(instance1 == instance2) {
System.out.println("创建的是同一个实例");
} else {
System.out.println("创建的是不同的实例");
}
}
}
class Singleton {
private static Singleton instance = null;
private String name;
public void setName(String inputName) {
name = inputName;
}
public void printName() {
System.out.println("我的名字是: " + name);
}
public static Singleton getSingleton() {
if(instance == null) {
synchronized(Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
private Singleton() {
System.out.println("开始构建单例");
}
}
上面的代码通过比较对象的地址来判断创建的两个对象到底是不是同一个对象。
运行结果如下:
开始构建单例
我的名字是: Example2
我的名字是: Example2
创建的是同一个实例
可见当第二次通过 setName 改变名字的时候,第一个实例也变为此名了。而且他们的对象地址都相同,所以说,单例无疑。
总结
开始以为单例很简单,只要把构造函数私有化不就行了么?后来才了解到并不是那么容易,还要考虑到许多别的问题,比如高并发,比如类加载机制,比如序列化。这里面的东西还有很多需要来挖掘。
特别致谢以下两位高人:
http://www.blogjava.net/kenzhh/archive/2013/03/15/357824.html