单例模式(从放弃到入门)

时间:2022-11-26 13:25:45

单例模式(从放弃到入门)

@(设计模式)

类图

单例模式(从放弃到入门)

简介

单例模式,可以说是设计模式中最简单的模式之一了,因为它不是用来解耦合用的,而是为了让整个系统有且只有一个这样的对象。为什么要只有一个呢??如果有些对象对象非常大,我需要保证内存中只存在一个;如果我希望做一些控制,只能由一个事例对象来控制,都会用到单例模式。

接下来我们就由浅入深的来探索单例模式的写法:

  1. 饿汉式
  2. 饱汉式
  3. 线程安全
  4. 双重检查加锁
  5. 静态内部类
  6. 枚举单例

问题

首先我们需要来考虑2件事情,既然单例模式需要保证系统中最多只有一个这样的对象事例,那么我怎么才能确保只能有一个实例呢??

  1. 构造方法:如果构造方法是 public,那么任何类都可以 new 我的对象,不能保证单例,所以单例模式构造方法一定是 private的
  2. 如果构造方法是 private的,那么我怎么 new对象呢?说明我只能在我自己的类中,先new出自己,然后提供给外部。
  3. 由于系统只有一个单例对象,那么线程同步是一定需要的,怎么来做呢?

就是因为上面的第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

  1. 为什么 构造方法是 private的? 因为上面提到的,不能由其他类任意new单例模式的类
  2. 为什么 getInstance() 是 static方法,因为用了 类名.来调用getInstance() 方法。
  3. 为什么 instance 是 static的? 静态方法可以访问非静态变量吗?显然不行,所以instance也是static的,而且静态变量是不是只会被初始化一次(感觉3个问题都是java考试题一样,哈哈哈!)
  4. 上面的第二种使用到了静态初始化块,在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;
}
}

这个版本看出优秀在哪里了吗?

  1. 懒加载
  2. 确保线程安全
  3. 只有第一次创建类的时候可能发生阻塞,后面由于非空判断都不会阻塞
  4. volatile是什么鬼?如果你关注线程安全的话就会了解,volatile是用来保证多个线程并发时,访问的都是内存中的同一个volatile对象。

缺点

仍然可以通过反射等方式产生多个对象!(其实我不懂,大家在后面评论之后我才知道 T . T)

问题4

  1. 为什么有两次非空判断?
    还是那个问题,时间片这个东西是不稳定的,由于线程切换,有可能两个线程同时都过掉第一层 非空判断,如果第二层不加,还是有可能调用2次new。

  2. 为什么是 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!