单例模式是笔试面试中最常考到的设计模式之一,当然在实际应用中也应该经常用到。利用单例模式,可以实现多个模块共享一个对象,节省内存的开销。如可以用单例模式来写一个配置文件管理类,不同模块要获取对应的配置信息的时候,获取的都是同一个配置文件管理对象。又或者是用单例模式写一个数据库连接对象,这样每次通过单例的工厂获取到的数据库连接都是同一个连接了。
废话不多说,直接进入正题,最简单的单例模式有两种模式,一种是懒汉模式,一种是饿汉模式。顾名思义,所谓懒汉模式就是在第一次代码调用的时候再生成第一个单例对象,而饿汉模式则是在类加载的时候已经把单例对象给生成出来放入内存中了。
先来看看饿汉模式:
public class HungrySingleton {
//用来保存单例实例的变量,类加载时就生成对象
private static final HungrySingleton instance = new HungrySingleton();
//下面是构造函数私有化,防止手动实例化。
//之前看到有一篇文章说这个其实是一种掩耳盗铃的方法,虽然构造方法已经私有化,
//不能直接用new来生成,但是还可以用代理的方法生成实例。不过我们暂时
//用这种方法吧,代理这东西还没深入研究过。
private HungrySingleton(){
}
//获取单例对象的工厂
public static HungrySingleton getInstance(){
return instance;
}
}
可以看到,instance是有初始化值的,折旧意味着jvm在准备阶段的时候会先调用私有构造函数生成一个HungrySingleton实例,然后让instance指向它。所以在你掉哟on个getInstance方法之前,虚拟机已经把单例的实例给生成好了。
这种方法在线程上是安全的,应为在还没创建线程的时候实例已经生成好了,就不存在资源竞争的问题。但是这样一来,在类被加载进去内存到第一次调用单例实例的过程中,就白白浪费了一个单例的实例的内存空间。
下面看看更加节省内存空间的懒汉模式:
public class LazySingleton {
//用来保存单例实例的变量,类加载时就生成对象
private static LazySingleton instance = null;
//私有化构造函数
private LazySingleton(){
}
//单例工厂
public static LazySingleton getInstance(){
if(instance == null){
instance = new LazySingleton();
}
return instance;
}
}
这种方式是在类加载的时候暂时把单例容器设为null,在第一次调用单例工厂的时候再生成一个单例实例,这种使用延迟生成实例的策略可以节省一部分时间段的内存空间。不过这种方法存在一个严重的错误,就是线程安全问题。我们运行下面代码验证一下。
public class Singleton {
//懒汉模式
private static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance() throws InterruptedException{
if(instance == null){
Thread.sleep(1000);//这里暂停1000ms,为了提高资源竞争的概率
instance = new Singleton();
}
return instance;
}
public static void main(String[] args){
//来两个线程来获取单例,然后把获得的对象打印出来
Thread thread1 = new Thread(new Runnable(){
public void run(){
Singleton s1;
try {
s1 = Singleton.getInstance();
System.out.println("thread1:"+s1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(new Runnable(){
public void run(){
Singleton s2;
try {
s2 = Singleton.getInstance();
System.out.println("thread2:"+s2);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
//线程开始
thread1.start();
thread2.start();
}
}
上面代码是启动两个线程去获取单例,我们对单例模式的期望是两个线程获取的对象是一样的,但是实际运行结果却是:
thread1:com.edin.dm.templateMethod.singleton.Singleton@3bc257
thread2:com.edin.dm.templateMethod.singleton.Singleton@153f67e
明显是不一样的两个对象,所以这种懒汉模式是非线程安全的。
如何要保持线程安全的前提下又节省内存空间呢?其实很简单,只要在获取实例的时候做一个线程同步的操作就行了,一种快刀斩乱麻式的方法是在单例工厂方法前面加上同步修饰符synchronized,这样当一个线程调用该方法的时候,其他线程就会阻塞,直到第一个调用该方法的的线程安全地把单例实例给生成出来了才,下一个线程开始就可以直接获取了。当然,也可以把线程同步写得更精巧点:
public class LazySingleton {
//用来保存单例实例的变量,类加载时就生成对象
private static LazySingleton instance = null;
//私有化构造函数
private LazySingleton(){
}
//单例工厂
public static LazySingleton getInstance(){
if(instance == null){ //只有第一次调用时才同步,其他的时候就跳过
synchronized(this.class){
if(instance==null){
instance = new LazySingleton();
}
}}return instance;}}
还有另外一种方法是非常神奇的方法就是定义一个内部静态类来实现,只能说写出这个方法的人,真的很牛!
//单例模式创新!google的ioc作者写的。只有在调用的时候才会初始化!而且线程安全
public class LazySingleton2 {
//内部静态类,在调用时加载
static class SingletonHolder{
static LazySingleton2 instance = new LazySingleton2();
}
//单例工厂
public LazySingleton2 getInstance(){
return SingletonHolder.instance;
}
}
JVM在类加载的时候是强制单线程的,同时在加载类的时候,其内部静态类是不加载,而是在其第一次被调用的时候才加载。