(面试总是需要手写单例模式,不得已只能总结一下)
什么是单例模式
简而言之,单例模式就是确保一个类只有一个实例(也就是类的对象),并且提供一个全局的访问点(外部通过这个访问点来访问该类的唯一实例)。
单例模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意点:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
具体实现
- 饿汉模式
所谓饿汉模式就是立即加载,一般情况下再调用getInstancef方法之前就已经产生了实例,也就是在类加载的时候已经产生了。这种模式的缺点很明显,就是占用资源,当单例类很大的时候,其实我们是想使用的时候再产生实例。因此这种方式适合占用资源少,在初始化的时候就会被用到的类。
class Singleton {
private static Singleton singleton = new Singleton ();
//将构造器设置为private禁止通过new进行实例化
private Singleton () {
}
public static Singleton getInstance() {
return Singleton ;
}
}
- 懒汉模式
懒汉模式就是延迟加载,也叫懒加载。在程序需要用到的时候再创建实例,这样保证了内存不会被浪费。针对懒汉模式,这里给出了5种实现方式,有些实现方式是线程不安全的,也就是说在多线程并发的环境下可能出现资源同步问题。
第一种:
// 单例模式的懒汉实现1--线程不安全
class Singleton {
private static Singleton singleton ;
private Singleton () {
}
public static Singleton getInstance() {
if (null == singleton ) {
try {
// 模拟在创建对象之前做一些准备工作
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
singleton = new Singleton ();
}
return singleton ;
}
}
第二种方法,我们使用synchronized关键字对getInstance方法进行同步。
// 单例模式的懒汉实现2--线程安全
// 通过设置同步方法,效率太低,整个方法被加锁
class Singleton {
private static Singleton singleton ;
private Singleton () {
}
public static synchronized Singleton getInstance() {
try {
if (null == singleton ) {
// 模拟在创建对象之前做一些准备工作
Thread.sleep(1000);
singleton = new Singleton ();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return singleton ;
}
}
这种方式达到了线程安全。但是缺点就是效率太低,是同步运行的,下个线程想要取得对象,就必须要等上一个线程释放,才可以继续执行。
那我们可以不对方法加锁,而是将里面的代码加锁,也可以实现线程安全。但这种方式和同步方法一样,也是同步运行的,效率也很低。
// 单例模式的懒汉实现3--线程安全
// 通过设置同步代码块,效率也太低,整个代码块被加锁
class Singleton {
private static Singleton singleton ;
private Singleton () {
}
public static Singleton getInstance() {
try {
synchronized (Singleton .class) {
if (null == singleton ) {
// 模拟在创建对象之前做一些准备工作
Thread.sleep(1000);
singleton = new Singleton ();
}
}
} catch (InterruptedException e) {
// TODO: handle exception
}
return singleton ;
}
}
接下来我们只给创建对象的代码加锁
// 单例模式的懒汉实现4--线程不安全
// 通过设置同步代码块,只同步创建实例的代码
// 但是还是有线程安全问题
class Singleton {
private volatile static Singleton singleton ;
private Singleton () {
}
public static Singleton getInstance() {
try {
if (null == singleton ) { //代码1
// 模拟在创建对象之前做一些准备工作
Thread.sleep(1000);
synchronized (Singleton.class) {
singleton = new Singleton(); //代码2
}
}
} catch (InterruptedException e) {
// TODO: handle exception
}
return singleton ;
}
}
我们假设有两个线程A和B同时走到了‘代码1’,因为此时对象还是空的,所以都能进到方法里面,线程A首先抢到锁,创建了对象。释放锁后线程B拿到了锁也会走到‘代码2’,也创建了一个对象,因此多线程环境下就不能保证单例了。
让我们来继续优化一下,既然上述方式存在问题,那我们在同步代码块里面再一次做一下null判断不就行了,这种方式就是我们的DCL双重检查锁机制。
//单例模式的懒汉实现5--线程安全
//通过设置同步代码块,使用DCL双检查锁机制
//使用双检查锁机制成功的解决了单例模式的懒汉实现的线程不安全问题和效率问题
//DCL 也是大多数多线程结合单例模式使用的解决方案
class Singleton{
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
try {
if (null == singleton) {
// 模拟在创建对象之前做一些准备工作
Thread.sleep(1000);
synchronized (Singleton.class) {
if(null == singleton) {
Singleton = new Singleton();
}
}
}
} catch (InterruptedException e) {
// TODO: handle exception
}
return singleton;
}
}
DCL双重检查锁机制很好的解决了懒加载单例模式的效率问题和线程安全问题.
- 静态内部类
//使用静态内部类实现单例模式--线程安全
class Singleton {
private Singleton() {
}
private static class SingletonHolder{
private static final Singleton singleton= new Singleton();
}
public static final Singleton getInstance() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return SingletonHolder.singleton;
}
}
和饿汉模式一样,是靠JVM保证类的静态成员只能被加载一次的特点,这样就从JVM层面保证了只会有一个实例对象。那么问题来了,这种方式和饿汉模式又有什么区别呢?不也是立即加载么?实则不然,加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。
- 静态代码块
//使用静态代码块实现单例模式
class Singleton{
private static Singleton singleton;
static {
singleton= new Singleton();
}
public static Singleton getInstance() {
return singleton;
}
}
- 序列化与反序列化
为什么要提序列化和反序列化呢?因为单例模式虽然能保证线程安全,但在序列化和反序列化的情况下会出现生成多个对象的情况。
//使用匿名内部类实现单例模式,在遇见序列化和反序列化的场景,得到的不是同一个实例
//解决这个问题是在序列化的时候使用readResolve方法,即去掉注释的部分
class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private static class InnerClass {
private static Singleton singleton = new Singleton();
}
//线程不安全
public static Singleton getInstance() {
return InnerClass.singleton;
}
//线程安全
protected Object readResolve() {
System.out.println("调用了readResolve方法");
return InnerClass.singleton;
}
}
当JVM从内存中反序列化地”组装”一个新对象时,就会自动调用这个 readResolve方法来返回我们指定好的对象了, 单例规则也就得到了保证。readResolve()的出现允许程序员自行控制通过反序列化得到的对象。
6. 枚举实现
class Resource{
}
public enum Singleton {
INSTANCE;
private Resource instance;
Singleton () {
instance = new Resource();
}
public Resource getInstance() {
return instance;
}
}
上面的类Resource是我们要应用单例模式的资源,具体可以表现为网络连接,数据库连接,线程池等等。
获取资源的方式很简单,只要 Singleton.INSTANCE.getInstance() 即可获得所要实例。下面我们来看看单例是如何被保证的:
首先,在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。
也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。
注意:枚举实现可以防止反射,反射时获取构造器会报错。
其实现原理都是利用借助了类加载的时候初始化单例。即借助了ClassLoader的线程安全机制。
所谓ClassLoader的线程安全机制,就是ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字。也正是因为这样,
除非被重写,这个方法默认在整个装载过程中都是同步的,也就是保证了线程安全。所以,以上各种方法,虽然并没有显示的使用synchronized,但是还是其底层实现原理还是用到了synchronized。
7.使用CAS实现
public class Singleton {
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();
private Singleton() {}
public static Singleton getInstance() {
for (;;) {
Singleton singleton = INSTANCE.get();
if (null != singleton) {
return singleton;
}
singleton = new Singleton();
if (INSTANCE.compareAndSet(null, singleton)) {
return singleton;
}
}
}
}
优缺点:
用CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。
CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。
另外,如果N个线程同时执行到singleton = new Singleton();的时候,会有大量对象创建,很可能导致内存溢出。
优缺点
优点:
1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
2、避免对资源的多重占用(比如写文件操作)。
缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景:
1、要求生产唯一序列号。
2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。