设计模式--单例模式
什么时候用单例模式:如果一个类没有自己的状态,也就是说无论你实例化多少个其实都是一样的,并且如果有多个实例的话可能会出现问题,此时应该考虑使用单例模式;
单例的优点:如果应用程序中有很多一模一样的对象,是对系统内存的浪费,而且容易导致错误;
单例的目的:尽可能的节约内存空间,减少GC消耗;
单例在项目中的使用: 配置文件信息,基础信息的加载;
普通单例模式代码示例:
public class Singleton { //静态实例 private static Singleton singleton; //私有构造方法,控制类的初始化 private Singleton() { } //静态方法,提供实例化的唯一途径(暂且不考虑反射突破) public static Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; } }
上面的代码是在不考虑并发的情况下标准的单例模式写法,解释下它为什么能保证拿到的实例是唯一的:
- 静态实例,使用static修饰的属性都是类中唯一的;
- 私有化构造方法,限制从外部随意创建实例,这个方法直接限制了无法通过构造方法构造此对象,只能通过我们提供的获取实例的方法来创建对象(暂且不考虑反射);
- 提供一个静态的公共的入口,因为这个方法要在对象创建之前给别人调用,如果不是静态那就自相矛盾了,原因是:非静态方法只有在对象实例化之后才能被调用;
- 通过提供的公共方法,判断静态实例为null的时候才调用私有的构造方法创建一个实例,否则将已有实例返回,单例完成;
上面是最简单的一种单例模式,如果在并发模式下会出现创建出多个实例,下面是代码实例和结果:
public class ConcurrentTestSingleton { volatile boolean lock; public boolean isLock() { return lock; } public void setLock(boolean lock) { this.lock = lock; } public static void main(String[] args) { final Set<String> instanceSet = Collections.synchronizedSet(new HashSet<String>()); final ConcurrentTestSingleton testSingleton = new ConcurrentTestSingleton(); testSingleton.setLock(true); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 100; i++) { executorService.execute(new Runnable() { @Override public void run() { while (true) { if (!testSingleton.isLock()) { Singleton singleton = Singleton.getInstance(); instanceSet.add(singleton.toString()); break; } } } }); } try { Thread.sleep(500); testSingleton.setLock(false); Thread.sleep(500); System.out.println("使用普通单例模式在100条线程并发的情况下取到的实例数量==="); for (String s : instanceSet) { System.out.println(s); //正常情况下应该只有一个实例字符串 } executorService.shutdown(); } catch (InterruptedException e) { e.printStackTrace(); } } }
下面是控制台输出结果:
使用普通单例模式在100条线程并发的情况下取到的实例数量===
desgin.singleton.Singleton@2aba21d2
desgin.singleton.Singleton@4b6697ab
desgin.singleton.Singleton@6b255c34
desgin.singleton.Singleton@6ab58489
看到结果就知道,控制台同时出现多个实例的字符串,说明这个普通的单例模式在并发的情况下并不可靠;
我们看下在并发情况下可靠的《Dubbo check》单例模式:
public class ConcurrentSingleton { //静态实例 volatile 禁止指令重排和标示可见性 强制应用程序从主内存中读取数据而非工作内存<具体volatile含义,请看另一篇博客> private volatile static ConcurrentSingleton singleton; //私有构造方法,控制类的初始化 private ConcurrentSingleton() { } //静态方法,提供实例化的唯一途径(暂且不考虑反射突破) public static ConcurrentSingleton getInstance() { if (singleton == null) { //1 synchronized (ConcurrentSingleton.class) {//2 if (singleton == null) { //3 singleton = new ConcurrentSingleton(); } } } return singleton; } }
同样使用上面的那段main方法测试程序,将线程数改为1000,运行10次,每次运行结果都只有一个实例字符串:
使用Dubbo check单例模式在1000条线程并发的情况下取到的实例数量===
desgin.singleton.ConcurrentSingleton@44ead434
解释下上面的程序为什么要双重检查是否为null:
首先我们假设有两条线程,A,B, 两条线程同时调用getInstance()方法,线程A B同时运行到//1 处,此时两条线程同时判断自己所持有的singleton为null,接下来假如说A先拿到了锁,这个时候B开始等待,A进入同步代码块判断singleton为null,
此时开始初始化对象,初始化完成之后释放锁,返回结果,但是因为A线程已经过了//1 处的判断,如果没有接下来的null 判断,那么A线程同样会重新再创建一个实例对象,这样情况下就会产生多个实例;代码中使用volatile来强制所有线程从主内存中读取数据,
这样也可以避免刚才说的那种情况,在A线程初始化成功后,因为对象是volatile修饰的,会直接刷新到主内存中,这个时候B线程再进入//3处判断的时候就会从主内存重新加载数据,发现singleton已经被初始化,会直接返回;
《此处要注意下,volatile是JDK1.5之后才被给予意义》
上面的程序虽然经过了简单的并发测试,好像没有问题了,但是从JVM层面来说,它还是有可能有问题的,虚拟机创建实例并不是原子性的,需要经过以下几步:
- 分配内存
- 初始化构造器
- 将对象指向分配的内存地址
这种正常的顺序是没有问题的,这种情况下JVM是完成了整个对象的构造才将内存的地址交给了对象,但是如果2 3 是相反的话(JVM可能针对字节码进行调优或指令重排),这时就会出现问题;
这种情况先将内存地址给对象,针对上述的双重加锁,就是说先将分配好的内存地址给concurrentSingleton,然后再进行初始化构造器,这时候后面的线程去请求getInstace方法时,会认为对象已经初始化,直接返回引用,如果在初始化构造器之前,这个线程使用了返回的引用,会产生莫名的问题;
下面的写法,将产生单例的任务交给jvm:
public class JVMSingleton { private JVMSingleton() { } public static JVMSingleton getInstance() { return SingletonInstance.instance; } private static class SingletonInstance { static JVMSingleton instance = new JVMSingleton(); } }
上面的代码保证了以下几点:
1: Singleton最多只有一个实例,不考虑反射突破的情况下,使用静态的属性和私有静态类保证jvm只进行一次加载,也就只有一次初始化;
2: 保证了并发情况下也同样不会产生多个实例;
3: 保证了并发访问的情况下,不会由于初始化动作还未完全完成而造成使用了尚未正确初始化的实例;
单例模式在开源框架中的应用:
spring的依赖注入(包括lazy-init方式)都发生在abstractBeanFactory的getBean()中,getBean的doGetBean方法调用getSingleton进行bean的创建,lazy-init是在容器初始化的时候创建,非lazy-init方式是在用户向容器第一次索要bean的时候进行调用,下面是单例在spring中的核心应用:
protected Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { synchronized (this.singletonObjects) { singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { ObjectFactory singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { singletonObject = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return (singletonObject != NULL_OBJECT ? singletonObject : null); }
从上面代码中看到,spring中使用了双重检查的单例模式,首先从缓存中获取bean实例,如果为null,对缓存map加锁,然后再从其中获取,如果还为null,就创建一个bean,这样的双重判断可以避免在加锁的瞬间有其他依赖注入引发bena的创建,
从而造成bean的重复创建;
另外在spring的aop中也有单例模式,比如AOP的切点定义中:
//饿汉单例模式
class TruePointcut implements Pointcut, Serializable {
private static final TruePointcut INSTANCE = new TruePointcut(); /** * Enforce Singleton pattern. */ private TruePointcut() { } .... public static TruePointcut getInstance(){ return INSTANCE; } }
上面的代码也可以改成枚举实现的单例模式:
enum TruePointcut implements Pointcut, Serializable { INSTANCE ; ... }
从普通单例模式,到并发的单例模式,再到jvm控制的单例模式,最后看了下经常使用的框架中对单例模式的应用,看似简单,其实并不简单!
2017-04-28 16:20:51

浙公网安备 33010602011771号