本文循序渐进介绍单例模式的几种实现方式,以及Jdk中使用到单例模式的例子,以及sring框架中使用到的单例模式例子。
饿汉式
package signgleton;
/**
* 单例模式简单的实现
*/
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
”饿汉式“只是形象的比喻,因为他想要这个实例的时候,不需要等待,别废话,给哥拿来。通过static的初始化方式,借助类第一次被加载时,就把Singleton实例给创建出来了,并存储在JVM的方法区,属于类变量,被所有的实例共享。不同的线程调用都返回一个实例,所以这样也保证了线程安全。
它还有个孪生兄弟,静态代码块来实例化:
package signgleton;
/**
* 通过静态代码块创建实例对象
*/
public class StaticSignleton {
private static StaticSignleton instance;
/**
* 静态代码块创建实例
*/
static {
instance = new StaticSignleton();
}
private StaticSignleton() {
}
public static StaticSignleton getInstance() {
return instance;
}
}
科普一下类初始化顺序:
- 静态变量、静态代码块初始化
- 构造函数
- 自定义构造函数
饿汉式缺点:因为在类被加载的时候对象就会被实例化,这可能会造成不必要的消耗,如果你的程序不在乎这点消耗那就当我没说。
下面介绍两种方式解决上面的问题:第一是使用静态内部类,第二是使用懒汉式
静态内部类
package signgleton;
/**
* 使用静态内部类获取单例实例
*/
public class StaticInnerClassSingleton {
private static class InnerSingletonClass{
private static final StaticInnerClassSingleton innerInstance = new StaticInnerClassSingleton();
}
private StaticInnerClassSingleton() {
}
public static final StaticInnerClassSingleton getStaticInstance() {
return InnerSingletonClass.innerInstance;
}
}
静态内部类同样借助了JVM这个大佬来保证线程安全,只是他在类加载的时候并没有立即实例化对象,而是采用了延迟加载策略,只有调用getStaticInstance的时候才用内部类去创建实例
线程不安全的懒汉式
package signgleton;
/**
* 线程不安全的懒汉式
*/
public class UnsafeSingleton {
private static UnsafeSingleton unsafeSingleton;
private UnsafeSingleton() {
}
public static UnsafeSingleton getUnsafeSingleton() {
if (unsafeSingleton == null) {
unsafeSingleton = new UnsafeSingleton();
}
return unsafeSingleton;
}
}
虽然这样写达到了使用的时候才实例化的目的,但是也带来的线程安全问题。在多线程下,可能有两个以上的线程同时进入if(unsafeInstance == null),比如有两个线程同时进入unsafeSingleton = new UnsafeSingleton();这是就会创建两个对象。
线程安全的懒汉式
package signgleton;
/**
* 线程安全的懒汉式
*/
public class SafeSingleton {
private static SafeSingleton safeSingleton;
private SafeSingleton() {
}
public static synchronized SafeSingleton getSafeSingleton() {
if (safeSingleton == null) {
safeSingleton = new SafeSingleton();
}
return safeSingleton;
}
}
这种方式在方法上加synchronized同步关键字解决了饿汉式线程安全问题,但是因为每次调用都加锁,极大地降低了性能,因为只有第一次创建实例时需要加锁,弄成现在每次都加锁。有没有解决办法呢,当然有,前辈们都是很聪明的,想出了双重校验锁这个经典的例子.
双重校验锁
package signgleton;
/**
* 线程不安全双重校验锁
*/
public class UnSafeTwoCheckSingleton {
private static UnSafeTwoCheckSingleton singleton;
private UnSafeTwoCheckSingleton() {
}
public static UnSafeTwoCheckSingleton getSingleton() {
if (singleton == null) { // ①
synchronized (UnSafeTwoCheckSingleton.class) { // ②
if (singleton == null) { // ③
singleton = new UnSafeTwoCheckSingleton();
}
}
}
return singleton;
}
}
双重校验锁的形式主要是缩小了锁的范围,但是熟悉多线程编程的同学就可以看得出来,即使这样做还是有线程安全问题,解决方案就是使用volatile,这其中体现了并发编程中的顺序性问题。我们来分析一下。 首先,我们来了解一下JVM创建对象的过程,比如下面的代码:
Student student = new Student();
加载步骤如下:
- new关键字会触发Student类的加载(如果已经加载,则次步骤作废)
- 根据Class对象中的信息,去开辟相应大小的内存空间。
- 初始化Student对象,就是完成成员变量的初始化操作(到这一步我们才能说对象是可用的)
- 将开辟出来的内存空间地址,复制给栈空间的变量student。
以上这些步骤对应的都是一些字节码指令,3、4步骤之间没有依赖关系,所以3、4执行顺序可能被JIT(即时编译器)重排序。
有了这些知识储备,我们接着来分析一下代码,首先假设有线程T1和T2,同时执行到①,因为此时singleton==null,T1进入到singleton = new UnSafeTwoCheckSingleton(); 执行new 操作的时候,假设T1刚好执行完上面加载步骤的步骤4(因为指令重排,所以4可能先比3执行)时间片段就没有了, 此时T2抢到了cpu分配的时间片,执行①,发现singleton已经有引用不为空,直接返回未初始化的singleton对象,注意此时的对象是没有经过步骤3初始化的,是一个不成熟的对象,使用的话就会报错。
使用volatile优化 首先补充一下volatile知识, volatile有两个作用:
1.禁止指令重排序 2.禁止使用CPU缓存
用作用1就可以解决双重检查锁的问题了。 我们来看下作用2,我们都知道,在CPU单核时代,线程1和线程2使用的都是同一个CPU缓存,所以线程之间的数据是可见的;在CPU多核时代,线程1在A核中,线程2在B核中,每个核都有自己的CPU缓存空间,如果线程1产生的数据缓存没有同步到线程2对应的CPU缓存,则会出现可见性问题。
package signgleton;
/**
* 线程安全双重校验锁
*/
public class SafeTwoCheckSingleton {
private static volatile SafeTwoCheckSingleton singleton;
private SafeTwoCheckSingleton() {
}
public static SafeTwoCheckSingleton getSingleton() {
if (singleton == null) {
synchronized (SafeTwoCheckSingleton.class) {
if (singleton == null) {
singleton = new SafeTwoCheckSingleton();
}
}
}
return singleton;
}
}
你以为这样就安全了吗,就想下班了吗?还没完,序列化这个恶棍会破坏单例,防范序列化这个恶棍破坏单例,可以在类中定义我们获取实例的策略,既加readResolve。
防范序列化破坏单例
package signgleton;
import java.io.Serializable;
/**
* 线程安全双重校验锁
*/
public class SafeTwoCheckSingleton implements Serializable{
private static volatile SafeTwoCheckSingleton singleton;
private SafeTwoCheckSingleton() {
}
public static SafeTwoCheckSingleton getSingleton() {
if (singleton == null) {
synchronized (SafeTwoCheckSingleton.class) {
if (singleton == null) {
singleton = new SafeTwoCheckSingleton();
}
}
}
return singleton;
}
private Object readResolve() {
return singleton;
}
}
单例模式案例
这么多的实现方式,你会问,有什么用?用处可大了,下面讲两个使用实例,一个jdk的Runtime, 一个是Spring框架中的单例模式。
Runtime:是一个封装了JVM进程的类,每一个JAVA程序实际上都是JVM的一个进程,每一个进程都是对应这么一个Runtime实例。 源码如下:
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
}
这里使用了饿汉式单例模式。
下面我们来看看看spring 中的单例模式,spring中使用的是单例注册表的特殊方式实现的单例模式,所以说模式是死的,需要灵活得运用。
看看单例注册表的实现原理demo:
package signgleton;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 单例注册表demo
*/
public class SingletonRegTest {
private final static Map<String, Object> singletonObjects = new ConcurrentHashMap<String,Object>();
/**
* 类加载时初始化一个实例
*/
static {
SingletonRegTest singletonRegTest = new SingletonRegTest();
singletonObjects.put(singletonRegTest.getClass().getName(), singletonRegTest);
}
public static SingletonRegTest getInstance(String name) {
if (name == null) {
// 默认分配一个实例
name = "signgleton.SingletonRegTest";
}
if (singletonObjects.get(name) == null) {
try {
// 将默认实例放入缓存中
singletonObjects.put(name, Class.forName(name).newInstance());
} catch (Exception ex) {
ex.printStackTrace();
}
}
return (SingletonRegTest) singletonObjects.get(name);
}
}
再来看看spring 源码:
public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
@SuppressWarnings("unchecked")
protected <T> T doGetBean(
final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
throws BeansException {
final String beanName = transformedBeanName(name);
Object bean;
// 从单例注册表中检查是否存在单例缓存
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
...
// 返回缓存实例
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}
else {
...
try {
...
// 如果是单例模式
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
...
}
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
// 如果是原型模式
else if (mbd.isPrototype()) {
...
}
// 其他模式
else {
...
}
}
catch (BeansException ex) {
...
}
}
return (T) bean;
}
}
我们进入 getSingleton()方法:
import java.util.Map;
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
// 通过 ConcurrentHashMap 实现单例注册表
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "'beanName' must not be null");
synchronized (this.singletonObjects) {
// 检查缓存中是否存在实例
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
...
try {
singletonObject = singletonFactory.getObject();
}
catch (BeanCreationException ex) {
...
}
finally {
...
}
// 如果实例对象在不存在,我们注册到单例注册表中。
addSingleton(beanName, singletonObject);
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
}
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));
}
}
}
是不是和我们的单例注册表demo很相似。 单例模式的讲解后面随着学习到其他框架再做相应的补充,也欢迎大家献言献策。