23种设计模式之单例模式以及在MyBatis中的应用
什么是单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
以上的话用我自己的语言来叙述就是:我们可以提供一种只能创建一个对象的类,这个对象在运行时内存中只能有一份。
接下来我们来看如何保证这个对象在运行时内存中只能有一份
单例模式的几种实现方式:
该模式的实现方式有多种,我将列出最经典的几种,其实知道了这几种其他的也就不难理解了:
1、懒汉式,线程不安全
public class Singleton {
private static Singleton instance;
private Singleton (){
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
在上面的代码中能实现单例的主要原因主要在于构造方法是私有的,在类的外部无法创建对象,然后只要在内部事先创建好本类对象,并通过对外开放的静态公有方法返回该内部创建的对象,但这里是线程不安全的,我们来解析一波为什么多线程下会破坏其中的单例:
当两个线程对这同一个getInstance方法进行调用时,由于在两个线程中instance此时没有引用任何对象,所以判断出instance都为空,所以在两个线程中都会返回不同的对象,显然这违背了单例,如果要是按照严格意义来说,我觉得上面不能称之为单例模式。
下面我再介绍几种线程安全的单例:
2、懒汉式,线程安全
public class Singleton {
private static Singleton instance;
private Singleton (){
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
上面的代码与上面唯一不一样的就是在getInstance方法前加上了synchronized关键字,意思是给这个方法加上了线程锁,每次只允许一个线程执行该资源,其他线程在外面等着!那么这样就不会出现上面这种创建多个对象的情况了,但是这样效率会大大折扣了。
下面再来另外一种饿汉式单例
3、饿汉式,天然线程安全
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){
}
public static Singleton getInstance() {
return instance;
}
}
这种给静态属性初始化的方式叫饿汉式,然后在外部调用该类getInstance方法获取该类静态对象。
为什么这种方式是线程安全的呢,我们再来解释一波为什么这是线程安全的:
当类在加载的时候类的静态属性就会初始化,要优先于其他代码,也就是说当我们调用getInstance时instance已在静态区了,所以返回的总是一个对象,不管几个线程同时调用都是一样,所以这个方法中也不用判断instance是否为空。
但是也不免会遇到利用其他方式导致了类的加载初始化,所以这种方式在某种情况下会产生垃圾对象
4、双检锁/双重校验锁(DCL,即 double-checked locking)
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){
}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
可以看到这个双重校验锁就是在懒汉模式的基础上加上了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绝对不会成立,因为其他线程已经创建好对象了。
5、登记式/静态内部类
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){
}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
显然这种方式可以与双重校验锁方法达到同样的效果,同时又与饿汉式相似都在类加载时实例化,不同的是登记式是一种延迟加载,就是在加载Singleton类是不会实例化INSTANCE对象,加载本类的静态内部类才会实例化,这样我们可以保证getInstance方法获取到的是一个唯一的示例对象。为什么饿汉式可能会发生多次加载而登记式不会呢?首先getInstance是一个静态方法,整个程序只有一份,当多个线程访问同一个getInstance时内部类SingletonHolder会保证只加载一次,所以这样又可以达到与双重校验锁一样高的效率
6、枚举,线程安全,完美
public enum Singleton {
OBJ;
private Object obj = null;
private Singleton(){
obj = new Object();
}
public Obj getInstance(){
return obj;
}
}
/*Test*/
public class Test {
public static void main(String[] args) {
Object obj1 = Singleton.OBJ.getInstance();
Object obj2 = Singleton.OBJ.getInstance();
System.out.println(obj1 == obj2); //结果为true
}
}
它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化
亲自测试如下:
通过反汇编可以发现下面这几行代码:
static {
OBJ = new Singleton("OBJ", 0);
$VALUES = (new Singleton[] {
OBJ
});
}
其实enmu底层是通过继承Enmu类来实现的,这是其中的一部分代码,其中源码与饿汉式的实现差不多,但是这种方式天然防反射,线程安全,效率高,可以使用这种方法。
这里太多的解释我也不做出来了,因为我本身没有深入研究过这里的源码
MyBatis中使用单例创建会话对象
我们首先要知道,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();
}
}
我只列出以上两个示例,其实知道了上面的两个示例,其他的方法自己也能够写的出来了,现在可以自己动手试一试了。