Hello,大家好,距离上次写博客是2018年1月26号,算了下,有8个月没写博客了。这里给大家道个歉,因为我换了工作,现就职在深圳一家公司,换了城市,加上工作上的一些事,所以一直抽不开身,2个月前不是太忙的时候,一直想着写点什么,可又找不到感觉了,所有就慢慢吞吞的,今天下定决心,写点渣渣也要写。由于博主最近准备把设计模式这一块好好整一整,所以就从最简单的 单例模式 开始,这个单例模式,说简单也简单,说难也难。有点生疏,大家看的时候,如果觉得写的不好,多多包含,OK,进入正题,文章结构:
所谓的单例模式,其实大家都知道,就是在应用程序中的某个类,无论在任何时间想拿到这个类的事例,都拿到的是唯一一个实例,那么问题来了,什么叫唯一的一个实例,说的再通俗点,就是拿到的那个指针(C,C++中叫指针,Java中叫引用对象)地址是唯一的。大家通过 new 关键字new几次new出来的对象,那肯定不是唯一的了。
上代码之前,先说一下,单例模式的几个要点:
OK,上代码了,因为太简单了,不知道怎么再展开了。
public final class Student { // 注意这里是私有化的 private Student() {} // 注意这里是私有化的 private static final Student INSTANCE = new Student(); // 暴露出去的方法 public static Student getInstance() { return INSTANCE; } } 复制代码
贼鸡儿简单,想拿Student对象的时候,直接Student.getInstance(); 不存在什么线程安全问题,因为类内部的static变量会在类加载的时候直接创建出来。你要 想整个静态代码快去初始化INSTANCE变量 ,其实也是一样一样的。这里就不写了。其实就是利用是static变量和static代码快在类加载时直接加载执行原理。
其实对于绝大多出场景,上面的饿汉已经绝对够用了。比如Spring框架中的bean,默认情况下就是单例的,就是直接给你new出来,然后丢在内存里,你要@Autowire的时候,直接给你。但这里有个小问题,有些类的初始化非常耗时,比如数据库链接,Redis链接等,这种网络IO操作。很有可能因为网络原因导致很耗时,在类被加载而他的实例还没有被使用的时候,上面的饿汉模式显然是不太合适的,如果这种耗时比较多的饿汉单例比较多的话,影响应用程序的启动时间。So,我们的懒汉上场了,OK,Code:
public final class Student { private Student() {} private static Student INSTANCE = null; public static Student getInstance() { //Mark 1 if(INSTANCE==null){ INSTANCE= new Student(); } return INSTANCE; } } 复制代码
代码也是贼鸡儿简单,在getInstance()中判断一下INSTANCE是不是null,如果是null(第一次调用)就初始化,下一次不是Null,返回旧的那个。写到这里,有点并发编程经验的小伙伴就知道了,在 Mark 1 的位置,当有多个线程同时进入的话,会有两个线程同时进入if()代码快,那么就糟糕了,INSTANCE会被初始化多次。不是线程安全的。接下来重头戏,我会演示几种线程安全的懒汉式单例模式:
直接上代码:
public final class Student { private Student() {} private static Student INSTANCE = null; // Mark 2 public static synchronized Student getInstance() { //Mark 1 if(INSTANCE==null){ INSTANCE= new Student(); } return INSTANCE; } } 复制代码
其实很简单,就是在getInstance()方法上加了synchronized关键字,这里synchronized关键字就不展开讲了,其实就是锁住了整个getInstance()方法,保证线程同步,这样当有多个线程进入这个方法时,会以Student.class为锁,只有一个方法先进去,然后后面的线程再进去的时候,INSTANCE已经不是null了,就进入不了if代码块。所以可以保证if代码块在多线程的情况下只进入一次,也就是说Student类只被实例化一次。
上面的synchronized锁住方法,其实是阔以的,但大家都知道synchronized关键字锁住方法的效率还是有点小问题的,毕竟每次调用这个方法都加锁,想想都很不爽 。效率不是很高,我就不多说了,所以这里推荐使用的是synchronized关键字的 双重锁检查 方式,代码如下:
public final class Student { private Student() {} // Mark 1 private static volatile Student INSTANCE = null; public static Student getInstance() { // Mark 2 if(INSTANCE == null) synchronized (Student.class){ // Mark 3 if(INSTANCE==null){ INSTANCE= new Student(); } } return INSTANCE; } } 复制代码
好了,来分析下这个代码,先看所谓的双重锁检查的Mark 2 和 Mark 3 ,如果没有Mark 2 的话,其实和在方法上加synchronized关键字的效果是一样一样的,效率比较差,每次进这个方法后,都加一波锁。如果没有Mark 3 的话,大家想一想,当有两个线程同时走到Mark 2 的位置,这时两个线程都进入第一个If代码快,然后在synchronized关键字的时候被锁住一个线程,另一个进去了,然后INSTANCE被初始化了,那么当这个线程出来后,另外一个被锁住的线程进去之后,如果没有Mark 3 的话,直接执行INSTANCE= new Student(); 又实例化了一个Student,这显然不是单例模式了。所有,Mark 2 和 Mark 3 的位置的if判断都是不能少的,这也就是所谓的双重锁检查了。这样即能保证Student类只被实例化一次,又能保证在安全的实例化后,后续getInstance()的时候不走有锁的代码了,是不是很完美。大家好好体会一下。最后值得一说的是,在Mark 1 的位置,必须保证这个INSTANCE变量是volatile 类型的,其实是为了保证线程可见性,保证第一个进入线程后的赋值操作,在后面的线程进入后,能够看到,也就是说Mark 3 位置能看到。给大家一个参考文章,解释为什么要volatile关键字的。 单例模式中用volatile和synchronized来满足双重检查锁机制
public final class Student { private Student() {} public static Student getInstance() { return StudentInner.INSTANCE; } private static class StudentInner { private static final Student INSTANCE = new Student(); } } 复制代码
其实就是利用了静态内部类的加载顺序问题,(静态内部类的加载顺序),只有在调用StudentInner.INSTANCE的时候,静态内部类才被加载,INSTANCE变量才会被实例化,而且,类的加载肯定是线程安全的,不用考虑volatile和synchronized的问题。有意思不?!哈哈。
好了,单例模式其实就差不多了,网上也有很多相似的文章,我其实就是做了个总结,加了点自己的理解,没什么技术含量,后面给大家写其他设计模式的时候可能有点技术含量,结合实际的案例,比如哪些框架里用到了,这个单例模式,最典型的就是Spring容器里的Bean了,要是再要举例的话,那就是System.getSecurityManager()了,这个也是单例的。这个类是管理Java Application的权限的,不怎么用,我们一般都是运行容器时管理权限,很少在代码里去控制。 Over,Have a good day !