Java单例模式
单例的好处
单例模式适合于应用中频繁创建的对象,如果是重量级的对象,更应该使用单例模式。比如配置文件,如果不采用单例模式的话,每个配置文件对象的内容都是一样的,创建重复的对象就会浪费宝贵的内存,所以有必要使用单例模式,达到性能的提升,减小了内存的开销和GC的压力。本文会一步一步由浅入深的讨论如何实现正确的单例模式。
单例模式的一般写法
- 饿汉式
public class HungryMode {
private static HungryMode sHungryMode = new HungryMode();
private HungryMode() {
System.out.println("create " + getClass().getSimpleName());
}
public static void fun(){
System.out.println("call fun in HungryMode");
}
public static HungryMode getInstance(){
return sHungryMode;
}
public static void main(String[] args) {
HungryMode.fun();
}
}
饿汉式单例,就是一个私有的构造方法加一个私有的静态当前类实例对象和一个公有的静态获取实例方法组成由于类实例对象为静态变量,所以在加载类的时候我们就会创建类的实例对象,这样的话比较消耗内存,浪费性能。
可以用HungryMode.fun()方法验证,在直接调用这个方法的时候,会加载HungryMode这个类到内存中,并且也会实例话静态的类实例对象。所以运行效果为
create HungryMode
call fun in HungryMode
如果这个对象的构造方法很复杂的话,这样的单例写法会造成类加载很慢,并且会浪费很多性能,所以我们需要懒加载,也就是所谓的懒汉式加载
- 懒汉式
public class LazyMode {
private static LazyMode sLazyMode;
private LazyMode() {
System.out.println("create " + getClass().getSimpleName());
}
public static LazyMode getInstance(){
if (sLazyMode == null) {
sLazyMode = new LazyMode();
}
return sLazyMode;
}
public static void main(String[] args) {
LazyMode.getInstance();
}
}
懒汉式在饿汉式的基础上做了改进,类实例对象做了懒加载,也就是所谓的延时加载,所以提升了一些性能。
多线程的单例
以上的写法在单线程的编程环境中没有什么问题,但是如果是多个线程使用上面的单例模式就会违背单例模式的设计原则,出现多个对象,简单的做法是在获取类实例的方法上加同步锁,并且给类实例对象加上volatile修饰符,volatile能保证对象的可见性,即在工作内存的内容更新能立即在主内存中可见。工作内存是线程独有的内存,主内存是所有线程共享的内存。还有一个作用是禁止指令重排序优化。大家知道我们写的代码(尤其是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同。这在单线程看起来没什么问题,然而一旦引入多线程,这种乱序就可能导致严重问题。volatile关键字就可以从语义上解决这个问题。如代码所示
public class LazyMode {
private static volatile LazyMode sLazyMode;
private LazyMode() {
System.out.println("create " + getClass().getSimpleName());
}
public static LazyMode getInstance(){
synchronized (LazyMode.class) {
if (sLazyMode == null) {
sLazyMode = new LazyMode();
}
}
return sLazyMode;
}
public static void main(String[] args) {
LazyMode.getInstance();
}
}
其实上面的代码还是有点性能问题的,因为同步锁的机制,多个线程获取类实例对象会排队等待获取锁,这样是没必要的,因为大多数情况下类实例对象都已经创建成功了,所以不用进入加锁的代码块,于是就可以再次改进上面的代码为双重校验的单例模式,如代码所示
/**
* 多线程的单例模式,使用双重校验机制
*/
public class DoubleCheckMode {
private volatile static DoubleCheckMode sDoubleCheckMode ;
public DoubleCheckMode() {
System.out.println(" created " + getClass().getSimpleName());
}
public static DoubleCheckMode getInstance() {
if (sDoubleCheckMode == null)
synchronized (DoubleCheckMode.class) {
if (sDoubleCheckMode == null) {
sDoubleCheckMode = new DoubleCheckMode();
}
}
return sDoubleCheckMode;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread() {
@Override
public void run() {
super.run();
System.out.println("thread" + getId());
DoubleCheckMode.getInstance();
}
}.start();
}
}
}
这样的写法就能够做到效率和安全的双重保证。但是有个问题,禁止指令重排优化这条语义直到jdk1.5以后才能正确工作。此前的JDK中即使将变量声明为volatile也无法完全避免重排序所导致的问题。所以,在jdk1.5版本前,双重检查锁形式的单例模式是无法保证线程安全的。不过现在的jdk环境大多数都是1.5之后的了,这个问题我们有个印象就行了。
使用静态内部类实例单例
这种实现就是利用静态类只会加载一次的机制,使用静态内部类持有单例对象,达到单例的效果,直接上代码吧
/**
* 静态内部类的方式实现单例,可以保证多线程的对象唯一性,还有提升性能,不用同步锁机制
*/
public class InnerStaticMode {
private static class SingleTonHolder {
public static InnerStaticMode sInnerStaticMode = new InnerStaticMode();
}
public static InnerStaticMode getInstance(){
return SingleTonHolder.sInnerStaticMode;
}
}
使用枚举实现单例
/**
* 利用枚举的方式实现单例,Android不推荐
*/
public enum EnumMode {
INSTANCE;
private int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
总结
枚举实现单例,不推荐在Android平台使用,因为内存消耗会其他方式多一些,Android官方也不推荐枚举,Android平台推荐双重校验或者静态内部类单例,现在的Android开发环境jdk一般都大于1.5了。所以volatile的问题不必担心。Java平台开发的Effective Java一书中推荐使用枚举实现单例,可以保证效率,而且还能解决反序列化创建新对象的问题。