Java中单例(Singleton)模式是一种广泛使用的设计模式。单例模式的主要作用是保证在Java程序中,某个类只有一个实例存在。
一些管理器和控制器常被设计成单例模式。
单例模式好处:
它能够避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间;能够避免由于操作多个实例导致的逻辑错误。如果一个对象有可能贯穿整个应用程序,而且起到了全局统一管理控制的作用,那么单例模式也许是一个值得考虑的选择。单例模式有很多种写法,大部分写法都或多或少有一些不足。下面将分别对这几种写法进行介绍。
我们看到双重校验锁即实现了延迟加载,又解决了线程并发问题,同时还解决了执行效率问题,是否真的就万无一失了呢?
这里要提到Java中的指令重排优化。所谓指令重排优化是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。JVM中并没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排序的优化。这个问题的关键就在于由于指令重排优化的存在,导致初始化Singleton和将对象地址赋给instance字段的顺序是不确定的。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。
JDK5的修正:以上就是双重校验锁会失效的原因,不过还好在JDK1.5及之后版本增加了volatile关键字。 volatile的一个语义是禁止指令重排序优化,也就保证了instance变量被赋值的时候对象已经是初始化过的,从而避免了上面说到的问题。Java中的volatile 变量是什么? 理解volatile关键字的作用的前提是要理解Java内存模型,volatile关键字的作用主要有两个: (1)多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据 (2)代码底层执行不像我们看到的高级语言—-Java程序这么简单,它的执行是Java代码–>字节码–>根据字节码执行对应的C/C++代码–>C/C++代码被编译成汇编语言–>和硬件电路交互,现实中,为了获取更好的性能JVM可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用volatile则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率 从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见java.util.concurrent.atomic包下的类,比如AtomicInteger。 CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。
volatile是一个特殊的修饰符,只有成员变量才能使用它。在Java并发程序缺少同步类的情况下,多线程对成员变量的操作对其它线程是透明的。volatile变量可以保证下一个读取操作会在前一个写操作之后发生。
来源: http://blog.csdn.net/fly910905/article/details/79283557
代码如下:
public class Singleton { private static volatile Singleton instance = null; private Singleton(){} public static Singleton getInstance() { if (instance == null) { // Single Checked synchronized (Singleton.class) { if (instance == null) { // Double checked instance = new Singleton(); } } } return instance; } }
ThreadLocal可以保证同一个线程中保留一份实例,对不同的线程,都有这个实例的一个副本,这样保证线程安全。在不考虑反射攻击与序列化与反序列化破坏的情况下,下面这一种单例模式的实现也有可取之处,尤其在高并发下通过空间换取时间更能提供性能的保证。
说明:这个模式其实并不是单例,但具有单例的形式,每个线程里面都是各自的副本对象(多个对象),互不影响而已,所以避开了多线程共享数据的问题,这也是容器设计的原理,每个请求相当于一个线程,每个线程持有各个请求的所有的数据,用完释放,这个是ThreadLocal内部弱引用的优势,垃圾回收能够即时发现回收!
class MyThreadLocalScopeDate{//单例模式 private MyThreadLocalScopeDate(){};//构造方法私有化 private static ThreadLocal<MyThreadLocalScopeDate> map = new ThreadLocal<MyThreadLocalScopeDate>();//封装MyThreadLocalScopeDate是线程实现范围内共享 //多线程过来都读取是自己内部的线程变量,所以不需要同步 public static /*synchronized*/MyThreadLocalScopeDate getThreadInstance(){ MyThreadLocalScopeDate instance =map.get(); if(instance==null){ instance = new MyThreadLocalScopeDate(); map.set(instance); } return instance; } }可以看到,枚举实现单例还是比较简单的,除此之外我们再来看一下Enum这个类的声明: public abstract class Enum<E extends Enum<E>>implements Comparable<E>, Serializable
可以看到,枚举也提供了序列化机制。某些情况,比如我们要通过网络传输一个数据库连接的句柄,会提供很多帮助。 最后借用 《Effective Java》一书中的话, 单元素的枚举类型已经成为实现Singleton的最佳方法。在这之前,单例模式,最常见的就是饥饿模式和懒汉模式,一个是直接实例化对象,一个在调用方法时候进行实例化对象,在多线程模式中,考虑到性能和线程安全问题,我们一般选用下面两种经典模式,在提高性能的同时,又保证了线程安全。 dubble check instancestatic inner class(静态内部类,更常用)
示例:
/** * @Title: java单例之enum实现方式 * @ClassName: EnumSingleton.java * @Description: * * @Copyright 2016-2018 - Powered By 研发中心 * @author: 王延飞 * @date: 2018-02-07 20:02 * @version V1.0 */ public class EnumSingleton{ private EnumSingleton(){} public static EnumSingleton getInstance(){ return Singleton.INSTANCE.getInstance(); } private static enum Singleton{ INSTANCE; private EnumSingleton singleton; //JVM会保证此方法绝对只调用一次 private Singleton(){ singleton = new EnumSingleton(); } public EnumSingleton getInstance(){ return singleton; } } public static void main(String[] args) { EnumSingleton obj1 = EnumSingleton.getInstance(); EnumSingleton obj2 = EnumSingleton.getInstance(); //输出结果:obj1==obj2?true System.out.println("obj1==obj2?" + (obj1==obj2)); } } 上面提到的四种实现单例的方式都有共同的缺点: 1)需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。2)可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。
而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。
首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:
(1)饿汉式:线程安全
(2)懒汉式:非线程安全
(3)双检锁:线程安全
(4)静态内部类:线程安全
(5)ThreadLocal:线程安全
(6)枚举:线程安全