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();
    }

}

我只列出以上两个示例,其实知道了上面的两个示例,其他的方法自己也能够写的出来了,现在可以自己动手试一试了。

猜你喜欢

转载自blog.csdn.net/qq_43538697/article/details/108392945
今日推荐