一、概述
保证一个类仅有一个实例,并提供一个全局访问点。单例要求:私有构造器、线程安全、延迟加载、序列化和反序列化安全、反射攻击
1.1、适用场景
1、在多个线程之间,比如servlet环境,共享同一个资源或者操作同一个对象。
2、在整个程序空间使用全局变量,共享资源。
3、在大规模系统中,为了性能的考虑,需要节省对象的创建时间等等。
想确保任何情况下都绝对只有一个实例
1.2、优缺点
单例对象(Singleton)是一种常用的设计模式。在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。这样的模式有几个好处:
1、某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。在内存里只有一个实例,减少了内存开销,可以避免对资源的多重占用
2、省去了new操作符,降低了系统内存的使用频率,减轻GC压力。
3、有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。
缺点:
1、没有接口,扩展困难
1.3、两种创建模式对比
饿汉式是典型的空间换时间,当类装载的时候就会创建类实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断了,节省了运行时间。
懒汉式是典型的时间换空间,延迟加载,也就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间。
1.4、安全发布的常用模式
可变对象必须通过安全的方式来发布,这通常意味着在发布和使用该对象的线程时都必须使用同步。如何确保使用对象的线程能够看到该对象处于已发布的状态,如何在对象发布后对其可见性进行修改。
安全地发布一个对象,对象的应用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布:
- 在静态初始化函数中初始化一个对象引用
- 将对象的应用保存到volatile类型的域或者AtomicReferance对象中
- 将对象的引用保存到某个正确构造对象的final类型域中
- 将对象的引用保存到一个由锁保护的域中。
在线程安全容器内部的同步意味着,在将对象放入到某个容器,例如Vector或synchronizedList时,将满足上述最后一条需求。如果线程A将对象X放入一个线程安全的容器,随后线程B读取这个对象,那么可以确保B看到A设置的X状态,即便在这段读/写X的应用程序代码中没有包含显式的同步。尽管Javadoc在这个主题上没有给出很清晰的说明,但线程安全库中的容器类提供了以下的安全发布保证:
- 通过将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器访问)
- 通过将某个元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中,可以将该元素安全地发布到任何从这些容器中访问该元素的线程
- 通过将某个元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程。
类库中的其他数据传递机制(例如Future和Exchanger)同样能实现安全发布,在介绍这些机制时将讨论它们的安全发布功能。
通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器: public static Holder holder = new Holder(42);
静态初始化器由JVM在类的初始化阶段执行。由于在JVM内部存在着同步机制,因此通过这种方式初始化的任何对象都可以被安全地发布[JLS 12.4.2]。
更多推荐看:java并发编程,相关介绍 对象可见性、对象的不变性、安全发布对象
二、详细说明
小结:一般情况下,推荐使用饿汉方式, 只有在要明确实现 lazy loading 效果时,才会使用静态内置类方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用双检锁方式。
2.1、饿汉模式【可以使用】
是否 Lazy 初始化:否;是否多线程安全:是;实现难度:易
描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
它的特点是加载类的时候比较慢,但运行时获得对象的速度比较快。它从加载到应用结束会一直占用资源。程序初始化的时候初始化单例。
public class EagerSingleton { //饿汉单例模式 //在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快 private final static EagerSingleton instance = new EagerSingleton();//静态私有成员,已初始化 private EagerSingleton() { //私有构造函数 System.out.println("new EagerSingleton"); } //静态,不用同步(类加载时已初始化,不会有多线程的问题) public static EagerSingleton getInstance() { return instance; } }
测试
@Test public void getInstance() { long start = System.currentTimeMillis(); ExecutorService fixedThreadPool = Executors.newFixedThreadPool(100); // Set<Integer> setObj = new HashSet<>(); //线程并发问题 ConcurrentSkipListSet<Integer> setObj = new ConcurrentSkipListSet(); for (int i = 0; i < 100; i++) { fixedThreadPool.submit(() -> { EagerSingleton instance = EagerSingleton.getInstance(); setObj.add(instance.hashCode()); }); } fixedThreadPool.shutdown(); System.out.println("耗时:"+(System.currentTimeMillis()-start)+"ms"); System.out.println("生成类数:"+setObj.size()); for (Integer s : setObj) { System.out.println("hashcode:"+s); } }