单例模式(从放弃到入门)
@(设计模式)
类图
简介
单例模式,可以说是设计模式中最简单的模式之一了,因为它不是用来解耦合用的,而是为了让整个系统有且只有一个这样的对象。为什么要只有一个呢??如果有些对象对象非常大,我需要保证内存中只存在一个;如果我希望做一些控制,只能由一个事例对象来控制,都会用到单例模式。
接下来我们就由浅入深的来探索单例模式的写法:
- 饿汉式
- 饱汉式
- 线程安全
- 双重检查加锁
- 静态内部类
- 枚举单例
问题
首先我们需要来考虑2件事情,既然单例模式需要保证系统中最多只有一个这样的对象事例,那么我怎么才能确保只能有一个实例呢??
- 构造方法:如果构造方法是 public,那么任何类都可以 new 我的对象,不能保证单例,所以单例模式构造方法一定是 private的
- 如果构造方法是 private的,那么我怎么 new对象呢?说明我只能在我自己的类中,先new出自己,然后提供给外部。
- 由于系统只有一个单例对象,那么线程同步是一定需要的,怎么来做呢?
就是因为上面的第2步,我们就出现了很多单例模式的写法,下面我们一一介绍:
饿汉式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance() {
return instance;
}
}
也可以这样写
public class Singleton {
private static Singleton instance;
static{
instance = new Singleton();
}
private Singleton(){}
public static Singleton getInstance() {
return instance;
}
}
client
Singleton s = Singleton.getInstance();
问题1
- 为什么 构造方法是 private的? 因为上面提到的,不能由其他类任意new单例模式的类
- 为什么
getInstance()
是 static方法,因为用了类名.
来调用getInstance() 方法。 - 为什么 instance 是 static的? 静态方法可以访问非静态变量吗?显然不行,所以instance也是static的,而且静态变量是不是只会被初始化一次(感觉3个问题都是java考试题一样,哈哈哈!)
- 上面的第二种使用到了静态初始化块,在Singleton第一次被使用的时候初始化。
懒汉式
public class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
if(null == instance){
instance = new Singleton();
}
return instance;
}
}
问题2
与饿汉式的不同:不是一看到 instance 就初始化,饱汉要等到第一次使用的时候才初始化,不像饿汉一样一见到 instance 就初始化,这也被称为 懒加载,如果系统中很多这样的类,显然是懒加载的时候效率更高
线程安全
如果对线程学得还行的同学就应该知道,上面的饱汉式写法其实不是那么的安全!!虽然在 getInstance() 中有非空判断,但是线程这个东西是由时间片控制的,如果一个线程刚刚非空判断通过了,然后切换到下个线程,也执行并通过非空判断,并切换到上一个线程,其实两个线程都会执行new操作,就不单例了!(虽然发生概率不高)但是我们为了更加的准确,我们来看看线程安全的版本怎么写
public class Singleton {
private static Singleton instance;
private Singleton(){}
public static synchronized Singleton getInstance() {
if(null == instance){
instance = new Singleton();
}
return instance;
}
}
问题3
只是在 getInstance() 前添加了 synchronized 关键字,保证同一时刻只能有一个线程执行获取到类锁,并执行创建实例代码,保证了只会创建一次。同时也有了懒加载的特性。
但是但是, 由于是单例模式,如果将锁直接锁在 static 的方法上(相当于类锁,其他static方法也不能执行),那么效率是非常低的,所以我们还有一定的改进。
双重检查加锁
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (null == instance) {
synchronized (Singleton.class) {
if (null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
这个版本看出优秀在哪里了吗?
- 懒加载
- 确保线程安全
- 只有第一次创建类的时候可能发生阻塞,后面由于非空判断都不会阻塞
- volatile是什么鬼?如果你关注线程安全的话就会了解,volatile是用来保证多个线程并发时,访问的都是内存中的同一个volatile对象。
缺点
仍然可以通过反射等方式产生多个对象!(其实我不懂,大家在后面评论之后我才知道 T . T)
问题4
为什么有两次非空判断?
还是那个问题,时间片这个东西是不稳定的,由于线程切换,有可能两个线程同时都过掉第一层 非空判断,如果第二层不加,还是有可能调用2次new。为什么是 synchronized (Singleton.class) 而不是 synchronized (this)?
有时java基本功,getInstance() 是 static 方法!static 方法!static 方法!,所以可以用this吗?
下面的内容是发之后网友给我的回复,感觉自己又被秀了一脸,主要还是书看得不够多,哈哈,做下面两种方式单例的补充
静态内部类
public class Singleton {
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
public static class SingletonHolder {
public static final Singleton instance = new Singleton();
}
}
原理就是说,静态内部类会在第一次被使用的时候被初始化,并且也只会被初始化一次,所以也包含懒加载和线程安全的特性。
枚举单例
public enum Singleton {
INSTANCE;
}
What the fuck? 就3行,好吧,我也是刚刚学的,解释一下原理,枚举类型也是在第一次被使用的时候初始化,并且默认构造函数是private修饰,而且线程安全。
序列化问题
传统单例写法,如果实现了序列化接口,他们就不能再保持单例,因为readObject() 方法每次会返回新的对象,所以你需要重写这个方法:
private Object readResolve(){
return INSTANCE;
}
参考文章:
http://blog.csdn.net/u014044853/article/details/52460735
http://www.importnew.com/6461.html
总结
还总个啥,反正就是感觉自己看书太少,被秀了,以后再看点高阶的书呗!《Effictive Java》I am coming!