单例模式
单例模式在程序设计中使用的频率非常之高,其设计的目的是为了在程序中提供唯一一个对象(保证只被构造一次),例如写入日志的log对象,windows的任务管理器实现(只能打开一个)。这里主要介绍单例模式使用Java的实现(包括饿汉式及懒汉式)。
实现
这里使用Log
类作为例子,Log
对象需要在程序中只有一个对象且只初始化一次。
饿汉式
饿汉式的单例模式理解起来是比较容易的,就是在单例类加载的时候就初始化需要单例的对象。实现也比较容易。
public class Singleton{
private static Log logObj = new Log();
public static Log getInstance(){
return logObj;
}
}
懒汉式
如果logObj
需要占用很大的内存,如果一开始就初始化logObj
,那么会占用大量的内存。此时,有人就想,如果我在想用的时候再初始化Log
类的对象,像懒汉一样,只有用到的时候再初始化,需要怎么设计呢?
实现一(非线程安全版本)
public class Singleton{
private static Log logObj = null;
public static Log getInstance(){
if(logObj == null){
logObj = new Log();
}
return logObj;
}
}
实现二(线程安全版本)
public class Singleton{
private static Log logObj = null;
public static synchronized Log getInstance(){
if(logObj == null){
logObj = new Log();
}
return logObj;
}
}
为了实现线程安全,这个版本的实现牺牲了一定的效率,如果logObj
已经初始化,那么其他线程还需要同步的进入getInstance
方法,会造成效率的损失。于是,有些人实现了下面的版本。
实现三(错误版本)
public class Singleton{
private static Log logObj = null;
public static Log getInstance(){
if(logObj == null){
synchronized(Singleton.class){
if(logObj == null){
logObj = new Log();
}
}
}
return logObj;
}
}
乍看起来上面的版本是没问题的,如果某个线程A
发现logObj
还没初始化,那么就进入同步块初始化logObj
,如果在这期间有其他线程B
进入,那么线程B
就会等待进入同步块,等待A
线程退出同步块,logObj
已经初始化了,B
线程进入同步块后发现logObj
不为null,退出同步块,不再初始化logObj
。 这样既实现了线程安全,又兼顾了效率,确实是很聪明的编码方式。但是问题来了,由于指令重排序的存在,会导致Log在完全初始化之前logObj就已经不为null。这样其他线程可能会得到未完全初始化的对象。
解决方法
- JDK1.5版本后扩展了volitile语义,可以保证上述代码的正确性,为此只要将
logObj
声明为volitile即可(volitile之前只是保证内存的可见性而已)。 - 使用静态内部类。
加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。
并且jvm会保证类加载的线程安全问题,所以利用这个特性可以写出兼顾效率与保证线程安全的版本。
实现四(兼顾效率与线程安全的版本)
public class Singleton{
static class LogHolder{
static Log logObj = new Log();
}
public static Log getInstance(){
return LogHolder.logObj;
}
}
这样在Singleton
类加载时,并不会加载LogHolder
,也就不会初始化Log
,如果有线程访问getInstance
方法,那么jvm会首先加载LogHolder
类,并保证初始化logObj
,最后返回logObj
。
版权声明:本文为博主原创文章,未经博主允许不得转载。