设计模式(一):单例模式

一、单例模式简介

单例模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

我们用静态类直接调用方法跟单例模式差不多,区别就是静态类直接调用是一种基于对象的思想,而单例模式则是一种面向对象的思想

概括意思就是:

  • 单例类只能有一个实例
  • 必须自己创建自己的唯一实例
  • 必须给其他对象提供这一实例

二、单例模式代码示例

单例模式的编写其实就是遵循三点:

  • 将构造函数私有化
  • 在类的内部创建实例
  • 提供获取唯一实例的方法
1.最基础的饿汉式
public class Singleton {

	//私有构造函数,限制用户主动创建实例
	private Singleton() {}
	private static Singleton instance = null;  //单例对象

	//静态工厂方法,用于获取Singleton实例
	public static Singleton getInstance() {
		//如果单例的初始值为null,则还为构建,构建单例对象并返回
		//否则直接返回
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}

这段代码不是线程安全的:
我们假设Singleton类刚被初始化,此时instance对象为空,两个线程同时访问getInstance()方法。此时两个线程经过判断都是instance == null,两个线程同时去执行 instance = new Singleton();,这样instance就被构建了两次,不符合我们说的单例类只有一个实例

2.直接加synchronized的写法

既然说了getInstance()方法会导致两个线程同时去创建,那我们直接加个synchronized关键字就好了:

public class Singleton {

	//私有构造函数,不可通过new来创建
	private Singleton() {}
	private static Singleton instance = null;  //单例对象

	//静态工厂方法,用于获取Singleton实例
	public static synchronized Singleton getInstance() {
		//如果单例的初始值为null,则还为构建,构建单例对象并返回
		//否则直接返回
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}

显而易见,这个写法效率比较低,于是有了下面的写法

3.双重校验锁写法

相对于直接给getInstance()synchronized,这种写法在多线程的情况下也能保证高性能

public class Singleton {

	//私有构造函数,不可通过new来创建
	private Singleton() {}
	//注意,此处需要加volatile来确保线程安全
	private static volatile Singleton instance;

	//静态工厂方法,用于获取Singleton实例
	public static Singleton getInstance() {
		//当singleton为空时就创建,否则返回
		if (instance== null) {
			synchronized (Singleton.class) {
				if (instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

注意

private static volatile Singleton instance;

这一行,给instance加了一个volatile才能保证安全,如果不加其实仍然有可能创建两个对象。
因为 instance = new Singleton();这个操作并不是原子操作,它分为以下三个操作:

1.分配内存空间
2.初始化对象
3.设置instance变量指向刚才分配的内存地址

由于指令重排序的存在,经过JVM和CPU的优化,有可能排成下面的顺序:

1.分配内存空间
3.设置instance变量指向刚才分配的内存地址
2.初始化对象

当线程A执行完1和3后,instance对象还未完成初始化,但此时已经不再指向null。此时线程B抢占到CPU资源,执行if (instance == null) 的结果返回false,从而返回一个未初始化完全的对象。

4.饿汉式单例

一旦创建Singleton就实例化,所以天生线程安全

public class HungrySingleton {
	private HungrySingleton() {}
	
	private static HungrySingleton hungrySingleton = new HungrySingleton();
	
	public static HungrySingleton getInstance() {
		return hungrySingleton;
	}
}
5.静态内部类的写法

效率最高,实现简单。利用了类加载机制来保证初始化instance时只有一个线程。

跟饿汉模式不同的是,饿汉模式只要Singleton被装载了那么instance就被初始化。而静态内部类的方式是懒加载的,Singleton被装载了,instance不一定被初始化(只有通过调用getInstance时,才会显示的装载Inner类来实例化instance)。如果实例化instance很耗费资源,所以想让它延迟加载,或者是不希望在Singleton加载时就实例化,因为不确定Singleton是否在其他地方被主动使用而被加载,这样采用这个方式就很合理.

public class Singleton {
	private static class Inner {
		private static final Singleton INSTANCE = new Singleton();
	}
	
	private Singleton() {}
	
	public static Singleton getInstance() {
		return Singleton.INSTANCE;
	}
}
6.使用ThreadLocal实现的线程安全的单例

使用ThreadLocal实现单例模式,ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。

对于多线程资源共享的问题,同步机制采用了以时间换空间的方式,而ThreadLocal采用了一空间换时间的方式。

前者只提供一个变量让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

public class Singleton {

	private static final ThreadLocal<Singleton> INSTANCE = new ThreadLocal<>() {
		@Override
		protected Singleton initialValue() {
			return new Singleton();
		}
	};

	private Singleton() {}

	public static Singleton getInstance() {
		return INSTANCE.get();
	}
}	
7.使用CAS的单例
public class Singleton {

	private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();

	private Singleton() {}

	public static Singleton getInstance() {
		for (; ; ) {
			Singleton current = INSTANCE.get();
			if (current != null) {
				return current;
			}
			current = new Singleton();
			//通过CAS,如果是空则设置为current
			if (INSTANCE.compareAndSet(null, current)) {
				return current;
			}
		}
	}
}	
8.一行代码的单例(枚举)
public enum Singleton {
	INSTANCE;
}

枚举类可以有效的防止多次实例化,而且JVM会阻止反射获取枚举类的私有构造方法。这种方式的缺点是跟饿汉式一样没有使用懒加载,其单例对象是在枚举类被加载的时候进行初始化的。

三、小结

在示例8中我们提到了反射机制可能会破坏单例并创建多个对象,我们对上面的8种单例模式进行分别反射测试,并总结其特性,结果如下:

单例模式是否线程安全是否懒加载是否防止反射构建
懒汉式单例
直接加synchronized
双重锁校验
饿汉式单例
静态内部类
使用ThreadLocal
CAS实现
枚举类实现

测试所用到的代码如下:

public class Test {

    public static void main(String[] args) throws Exception {
        //获取构造器
        Constructor constructor = Singleton.class.getDeclaredConstructor();
        //设置为可访问
        constructor.setAccessible(true);
        //构建两个对象
        Singleton singleton1 = (Singleton) constructor.newInstance();
        Singleton singleton2 = (Singleton) constructor.newInstance();
        //验证是否相同
        System.out.println(singleton1.equals(singleton2));
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值