一、认识单例模式
单例设计模式(Singleton):就是采用一定的方法保证整个软件系统中,对某个类只能存在一个对象实例,取得对象实例不能通过构造器来获取,只能通过一个方法取得实例。
实际应用场景:
计算机系统:windows回收站、操作系统中的文件系统、多线池中的线程池、显卡的驱动程序对象、打印机的后台处理程序、应用程序日志对象、数据库的连接池、网站的计数器、web应用的配置对象、应用程序中对话框、系统中缓存等。
现实生活:公司CEO、部门经理
J2EE标准:ServletContext和ServletContentConfig。
Spring框架:依赖注入bean实例(singleton缺省是饿汉式)、ApplicationContext、数据库连接池等。
优点:
保证内存只有一个实例,减少内存开销。
避免对资源多重应用。
设置全局访问点,可以优化和共享资源访问。
缺点:
单例模式无接口,扩展困难。若要扩展,除了修改原来代码,没有其他途径,违背开闭原则。
开闭原则:规定对象(类、模块、函数)对于扩展应该是开放的,修改是封闭的。
并发测试中,不利于代码调试。调试过程中,若之前一个线程中代码没有执行完,就不能模拟生成一个新的对象。
单例模式功能代码一般写在一个类中,若功能设计不合理,容易违背单一职责原则。
单一职责原则:指一个类只负责一项职责。
目的:一个类中只能有一个实例。
实现过程:若是一个类中只能有一个实例,那么其构造器肯定不是公共能够使用的,构造器不能外部使用,获取到实例的方式只能是通过一个静态方法获取,那么其中的实例也应当是静态的。
单例方式也分为懒汉式与饿汉式,他们各自创建实例的时机也是各不相同的,各有优势与缺点,对于使用普通类来创建单例都会存在安全问题(反射造成的),通过使用自定义枚举类(Enum)来创建单例解决反射安全问题!
二、三种实现方式
实现方式一:饿汉式
见singleton目录下的Hungry:
class Person{ //定义静态变量,并且进行实例化 private static Person person = new Person(); //构造器设置为私有,外界无法通过new来实例化 private Person() { } //定义静态方法,方便外部获取实例 public static Person getInstance(){ return person; } }
好处:因为本身实例是在类加载时创建的,所以是线程安全的。
坏处:会占用较多的空间
实现方式二:懒汉式(静态属性)
简单实现懒汉式(有线程安全问题)
下面是通过懒汉式方法来获取单例,仅仅做了一个是否为null的判断:
public class Person { private Person(){ System.out.println(Thread.currentThread().getName()); } private static Person PERSON; //获取Person类的单例对象 public static Person getInstance(){ if(PERSON == null){ PERSON = new Person(); } return PERSON; } //多线程来获取单例 public static void main(String[] args) { for(int i = 0;i<10;i++){ new Thread(()->Person.getInstance()).start(); } } }
此时提出问题:当多个线程同时调用该方法时是否依旧会获取到一个单例对象?
我们在main方法中使用多个线程调用Person.getInstance()方法,并且在无参构造器中添加一条输出语句,这样我们就能够很清晰的看到调用了几次构造方法!
从结果来看果然在多线程情况下调用获取单例方法通过使用简单判断的方式是不行的,因为在多线程情况下,CPU会给指定线程分配时间片(约100ms)一旦时间片结束,就切换另一个线程执行,所以当我们使用多个线程调用方法时,由于时间片太短,在执行new实例前多个线程已进入到if(PERSON == null)的方法体中,造成多次调用空参构造器,出现了线程安全问题。
同步方法、同步代码块(留有指令重排问题)
我们通过采用同步方法、同步代码块理论上能够解决线程安全问题,不过还存在一个指令重排的情况。
采用同步方法:
class Person{ //静态变量初始为null private static Person person = null; //构造器为私有 private Person() { } //获取person实例(线程同步) public static synchronized Person getInstance(){ if(person == null){ person = new Person(); } return person; } }
好处:延迟对象的创建。
坏处:执行速率慢,因为其整个方法是带锁的,那么多个线程初始调用方法时就会等待锁释放。
同步代码块:
class Person{ private static Person person; private Person(){ } public static Person getInstance(){ //方式一:外面包裹同步代码块 // synchronized (Person.class){ // if(person == null){ // person = new Person(); // } // } //方式二:进行双重加锁 if(person == null){ synchronized (Person.class){ if(person == null){ person = new Person(); } } } return person; } }
方式一:效率稍高,不过对于多线程来说需要每次进入到同步代码块中,并且包含了等待的时间,后边的线程需要等待前面的线程开锁了之后才能进(与同步方法一样)。
方式二:效率相对于方式一会提高,采用了双重加锁(即synchronized内外都进行if判断),在synchronized内进行if判断是为了预防创建多个实例,否则依旧会出现线程安全问题。
举例子说明:当线程A进入到synchronized代码块中,此时线程B、C也通过了外层if判断停留在synchronized外等待释放锁,一旦线程Anew完实例释放锁的话,就会又有一个线程再次进入为了预防其再次new实例,所以再次加一个if判断!
问题描述:这里实际上已经解决了多次调用空参构造器创建对象,不过依旧隐藏安全问题即为指令重排情况!见下部分说明!
双重加锁+volatile(解决指令重排)
涉及到Java内存模型,volatile关键字以及CPU指令重排概念
见singleton目录下的Lazy类:
指令重排:是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序。
举例说明:例如执行a=b+c;d=b+e;两条指令,在汇编中对于一条指令是可以分为很多步骤的,步骤如下:
a=b+c:①b加载到寄存器R1;②c加载到寄存器R2;③R1+R2放置到R3;④将R3放置到a; d=e+f:⑤e加载到寄存器R4;⑥f加载到寄存器R5;⑦R4+R5放置到R6;⑧将R6放置到d; 在cpu中为了提升性能会考虑指令重排的方式 1、不使用指令重排:依次执行顺序①②.③.④⑤⑥.⑦.⑧,其中.表示等待时间(数据加载到内存),也就是说如果不使用指令重排会有等待时间。为了提升性能CPU使用指令重排。 先介绍下指令重排发生情况:只可能发生在毫无关系的指令之间, 如果指令之间存在依赖关系, 则不会重排。上面例子中③依赖于①②,④依赖于③,而对于①②、⑤⑥则互不依赖,所以可以将⑤⑥移到前面 2、使用指令重排:执行顺序变为①②⑤③⑥④.⑦⑧,可以看到能够减少了几个时间周期。
通过上面的说明我们了解到指令重排是什么了之后就可以去探究使用同步方法依旧留有的问题了。—参考文章Java内存模型与指令重排
Volatile:禁止指令重排序,volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,在读取volatile类型的变量时总会返回最新写入的值。
问题描述:当线程A执行new操作实际上在底层会执行三个步骤①分配内存空间;②执行构造方法,初始化对象;③将该对象指向这个空间。Java内存模型是允许编译器对操作顺序进行指令重排的,原本顺序是①②③,通过指令重排后变为①③②,若是在执行完③没有执行②时(也就是说先给对象指向了空间,但并没有进行初始化操作)此时线程B进行了if判断,发现不为null,则直接返回,由于此空间没有完成构造还是空的那么就会出现大问题!
解决方案:给对象添加volatile修饰符来禁止指令重排,保证原子性操作来解决该安全问题。
class Person{ //添加volatile关键字来禁止指令重排解决安全问题,保证可见性 private volatile static Person person = null; //构造器为私有 private Person() { } //采用了双重加锁 public static Person getInstance(){ if(person == null){ synchronized (Person.class){ //这里是防止一个线程创建实例之后,另一个线程进入同步代码块时没有创建实例进入 if(person == null){ person = new Person(); } } } return person; } }
这里与之前懒汉式不同的是,这里采用了双重加锁,并且person属性增加了volatile原子性。
实现方式三:内部静态类方式(懒汉式)
通过创建一个静态内部类,在该内部类中静态属性来获取一个Instance实例:
public class Instance { private Instance(){} public static Instance getInstance() { return Holder.instance; } public static class Holder { private static Instance instance = new Instance(); //可以放开继续测试 // static { // System.out.println("666"); // } } public static void main(String[] args) { //懒加载:只有当真正去获取实例时才会去触发Holder初始化 System.out.println(Instance.getInstance()); } }
这种方式级为巧妙,很多框架内部都使用这种方式来实现单例模式,只有当我们去调用Holder的instance方法时才会进行Holder中Instance属性的实例化。
三种实现方式存有问题说明
问题描述1:我们通过反射的手段依旧能够破坏上面的单例,通过反射能够取消java语言访问检查,从而能够创建新的实例,我们就拿懒汉式(双重加锁+volatile)下手看看:
public class Person { private volatile static Person PERSON; private Person(){ System.out.println(Thread.currentThread().getName()); } public static Person getInstance(){ if(PERSON == null){ synchronized (Person.class){ if(PERSON == null){ PERSON = new Person(); } } } return PERSON; } //反射来破坏单例类的结构 public static void main(String[] args) throws Exception { //1、通过实例方法来获取 Person instance = Person.getInstance(); Constructor<Person> constructor = Person.class.getDeclaredConstructor(); //取消构造器访问检查 constructor.setAccessible(true); //2、反射来获取实例 Person person = constructor.newInstance(); System.out.println(instance == person); } }
可以很明显看到调用了两次空参构造器,也就是说创建了多个实例!
解决方案1:在空参构造器里加上判断
private Person(){ synchronized (Person.class){ //当PERSON实例被创建时就抛出异常 if(PERSON != null){ throw new RuntimeException("请不要做坏事情"); } } System.out.println(Thread.currentThread().getName()); }
在构造器里加上对PERSON实例判断后,再次使用原本的程序测试,我们能够看到果然成功抛出异常了!
问题描述二:若是我不调用Person.getInstance(),其中PERSON就不会被被赋予实例,接着我多次反射不就又能获取多个实例了吗?
public static void main(String[] args) throws Exception { Constructor<Person> constructor = Person.class.getDeclaredConstructor(); constructor.setAccessible(true); //反射获取实例1 Person instance = constructor.newInstance(); //反射获取实例2 Person person = constructor.newInstance(); System.out.println(instance == person); }
我们能够看到若是PERSON始终为空,依旧能够new出多个实例出来!
解决方案二:
针对于问题2,我们再次修改代码,这次并不对于PERSON来进行判断,而是限定构造器仅仅使用一次!
private static boolean flag = false; private Person(){ synchronized (Person.class){ if(!flag){ flag = true; }else{ throw new RuntimeException("不要企图通过反射多次new实例"); } } System.out.println(Thread.currentThread().getName()); }
针对于问题2我们看似解决了其实我们若是在第一个newInstance()实例后通过反射修改对应flag的布尔值为true,则依旧会失效!!!
总结:总而言之通过反射技术,我们创建的单例类使用是存在安全问题的!!!那么如何解决呢,我们可以通过使用枚举类来实现单例!!!