单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
以上的话用我自己的语言来叙述就是:我们可以提供一种只能创建一个对象的类,这个对象在运行时内存中只能有一份。 接下来我们来看如何保证这个对象在运行时内存中只能有一份
该模式的实现方式有多种,我将列出最经典的几种,其实知道了这几种其他的也就不难理解了:
在上面的代码中能实现单例的主要原因主要在于构造方法是私有的,在类的外部无法创建对象,然后只要在内部事先创建好本类对象,并通过对外开放的静态公有方法返回该内部创建的对象,但这里是线程不安全的,我们来解析一波为什么多线程下会破坏其中的单例: 当两个线程对这同一个getInstance方法进行调用时,由于在两个线程中instance此时没有引用任何对象,所以判断出instance都为空,所以在两个线程中都会返回不同的对象,显然这违背了单例,如果要是按照严格意义来说,我觉得上面不能称之为单例模式。 下面我再介绍几种线程安全的单例:
上面的代码与上面唯一不一样的就是在getInstance方法前加上了synchronized关键字,意思是给这个方法加上了线程锁,每次只允许一个线程执行该资源,其他线程在外面等着!那么这样就不会出现上面这种创建多个对象的情况了,但是这样效率会大大折扣了。 下面再来另外一种饿汉式单例
这种给静态属性初始化的方式叫饿汉式,然后在外部调用该类getInstance方法获取该类静态对象。 为什么这种方式是线程安全的呢,我们再来解释一波为什么这是线程安全的: 当类在加载的时候类的静态属性就会初始化,要优先于其他代码,也就是说当我们调用getInstance时instance已在静态区了,所以返回的总是一个对象,不管几个线程同时调用都是一样,所以这个方法中也不用判断instance是否为空。 但是也不免会遇到利用其他方式导致了类的加载初始化,所以这种方式在某种情况下会产生垃圾对象
可以看到这个双重校验锁就是在懒汉模式的基础上加上了volatile和synchronized修饰,这种方式线程安全,而且还能在多线程中保证程序的效率,那么我们来解释一下这种机制原理: 首先可以看到我们的静态属性加上了volatile关键字修饰,这样可以防止指令重排序造成的线程不安全问题,比如singleton在没有加volatile的情况下,对象在线程A中发生new操作时发生了指令重排,因为new大致可以分为3个步骤,1.开辟内存,2.将这片内存初始化,3.使singleton引用指向这片内存,我们可以发现这并不是一个原子操作,2和3没有依赖,所以2和3会发生重排序,那么这两个发生了重排序的话,B线程会发现这个对象初始化不成功,这个时候就会发生异常,所以我们就得加上volatile防止指令重排序。 现在再来看看方法中的那两个双重校验,也就是那两个if:第一个if若是不成立则100%创建好该对象了,这样就不会经过这个锁,这也就是效率高的原因,接下来就是锁中的if,其实这个if会在多个线程同时访问时都为null是才会走到这里来,也就是第一次实例化的时候,然后只要有一个线程出了这个锁,那么其他线程进锁这个if绝对不会成立,因为其他线程已经创建好对象了。
显然这种方式可以与双重校验锁方法达到同样的效果,同时又与饿汉式相似都在类加载时实例化,不同的是登记式是一种延迟加载,就是在加载Singleton类是不会实例化INSTANCE对象,加载本类的静态内部类才会实例化,这样我们可以保证getInstance方法获取到的是一个唯一的示例对象。为什么饿汉式可能会发生多次加载而登记式不会呢?首先getInstance是一个静态方法,整个程序只有一份,当多个线程访问同一个getInstance时内部类SingletonHolder会保证只加载一次,所以这样又可以达到与双重校验锁一样高的效率
它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化 亲自测试如下: 通过反汇编可以发现下面这几行代码:
static { OBJ = new Singleton("OBJ", 0); $VALUES = (new Singleton[] { OBJ }); }其实enmu底层是通过继承Enmu类来实现的,这是其中的一部分代码,其中源码与饿汉式的实现差不多,但是这种方式天然防反射,线程安全,效率高,可以使用这种方法。 这里太多的解释我也不做出来了,因为我本身没有深入研究过这里的源码
我们首先要知道,mybatis与数据库进行交互时需要获取xml文件中的数据库连接信息,整个获取对象就是一个工厂模式,但是我们希望这个对象只有一份,因为这样就可以达到更高的效率,和节省更多的空间,因为在频繁的读取xml时会消耗很多性能,所以我们把读取文件作为单例,先看如下代码:
public class MyBitesUtil { //工厂,可以生产SqlSession对象 static SqlSessionFactory factory = null; //静态代码块 static { try { InputStream is = Resources.getResourceAsStream("config/mybatis-config.xml"); factory = new SqlSessionFactoryBuilder().build(is); } catch (IOException e) { e.printStackTrace(); } } public static SqlSession getSqlSession() { return factory.openSession(); } }由于factory是只有单份的了,所以再关闭了factory之后,在次调用getSqlSession方法就不用再次重新读取xml文件了,只要将关闭后的factory对象再次open就ok了 还提供一种方式:
public class MyBitesUtil { private static MyBitesUtil factory; /** * 私有化构造 */ private MyBitesUtil(){ } /* * 单实例对象 */ public static MyBitesUtil init(){ String resource = "mybatis-config.xml"; InputStream inputStream = null; try { inputStream = Resources.getResourceAsStream(resource); }catch (IOException e){ e.printStackTrace(); } synchronized (MyBitesUtil.class){ if(factory == null){ factory = new SqlSessionFactoryBuilder().build(inputStream); } } return factory; } public static SqlSession getSqlSession(){ if (factory == null){ init(); } return factory.openSession(); } }我只列出以上两个示例,其实知道了上面的两个示例,其他的方法自己也能够写的出来了,现在可以自己动手试一试了。