多线程安全单例模式

什么是单例模式?

  在文章开始之前我们还是有必要介绍一下什么是单例模式。单例模式是为确保一个类只有一个实例,并为整个系统提供一个全局访问点的一种模式方法。

从概念中体现出了单例的一些特点:

(1)、在任何情况下,单例类永远只有一个实例存在
(2)、单例需要有能力为整个系统提供这一唯一实例

1. 全局变量的缺点:

  必须在程序一开始就创建好对象,如果程序在这次的执行过程中又一直没用到它,就非常耗费资源。

2. 经典的单例模式实现:

public class Singleton {    
      //用一个静态变量来记录Singleton类的唯一实例 
      //1、提供私有的属性  --> 存储对象的地址  
      private static Singleton uniqueInstance;
      
      //2、私有地构造方法  --> 只能本类可以创建对象
      private Singleton() {} 
           
      //注意这个方法也是静态的  -->获取属性 
      public static Singleton getInstance() {    
           if(uniqueInstance == null) {   
           		uniqueInstance = new Singleton();   
           }   
           return uniqueInstance;   
      }   
} 

单例常被用来管理共享的资源,例如 数据库连接、线程池、缓存、注册表, Spring中大量用到单例模式。
单例模式确保一个类只有一个实例,并提供一个全局访问点。
这个模式的问题:在多线程时,并不能保证这个类只被实例化一次。

3. 处理多线程:

public class Singleton {    
    //用一个静态变量来记录Singleton类的唯一实例   
    private static Singleton uniqueInstance;   
    
    private Singleton() {}   
           
    //注意这个方法也是静态的  使用synchronized线程同步  
    public static synchronized Singleton getInstance() {    
        if(uniqueInstance == null) {   
             uniqueInstance = new Singleton();   
         }   
         return uniqueInstance;   
    }   
}  

  通过增加synchronized关键字到**getInstance()**方法中,迫使每个线程在进入方法之前,要先等别的线程离开该方法。也就是说,不会有两个线程可以同时进入这个方法。

这种方法存在的问题:只有第一次执行此方法时,才真正需要同步。换句话说,一旦设置好uniqueInstance变量,就不再需要同步这个方法了。之后每次调用这个方法,同步都是一种浪费。

4.改善多线程

4.1 如果getInstance()的性能对应用程序不是很关键,就不用优化了
4.2 使用急切创建实例,而不用延迟实例化的做法
public class Singleton {
	//实例化时就创建对象  单例模式中的饿汉式
    private static Singleton uniqueInstance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {    
         return uniqueInstance;   
    }
}

   private static Singleton uniqueInstance = new Singleton();在静态初始化器(static initializer)中创建单例,这保证了线程安全。
  利用这个做法,JVM在加载这个类时马上创建此唯一的单件实例。JVM保证任何线程访问uniqueInstance静态变量之前,一定先创建些实例。

4.3 用“双重检查加锁”,在getInstance()中减少使用同步

  首先检查实例是否已经创建,如果尚未创建,才进行同步。这样一来,只有第一次会同步,这正是我们想要的。

public class Singleton {
    //volatile其他线程可能访问一个没有初始化的对象
    private volatile static Singleton uniqueInstance;
    private Singleton() {}
    public static Singleton getInstance() {    
	    if(uniqueInstance == null) {  //(1)
			//只有第一次才彻底执行这里的代码   
	       synchronized() {   
	          //再检查一次   
	          if(uniqueInstance == null)   
	       		 uniqueInstance = new Singleton();   
	       }   
	    }   
        return uniqueInstance;   
    }   
}  

  在最开始如果有1、2、3个线程走到了(1)处,假设1进入了同步块,2、3等待。1实例化后,2进入同步块,发现uniqueInstance已经不为空,跳出同步块。接着3进入,又跳出同步块。
  volatile关键字确保:当uniqueInstance变量被初始化成Singleton实例时,多个线程正确地uniqueInstance变量。如果性能是你关心的重点,那么这个做法可以帮你大大地减少getInstance()的时间耗费。
  最后给一个完整的demo:

package com.gqz.thread;
/*
 * 单例模式:懒汉式套路 在多线程环境下  对外存在一个对象
 * 1、构造器私有化 --> 避免外部 new构造器
 * 2、提供私有地属性  --> 存储对象的地址
 * 3、提供公共的静态方法--->获取属性
 */
public class DoubleCheckedLocking {
	//2、提供私有地属性
	//直接new对象了是饿汉式  没有new的事懒汉式
	//volatile其他线程可能访问一个没有初始化的对象
	private static volatile DoubleCheckedLocking instance; 
	
	//1、构造器私有化
	private DoubleCheckedLocking() {
		
	}
	
	//3、提供公共的静态方法-->获取属性
	public static DoubleCheckedLocking getInstance() {
		//再次检测
		if (null != instance) { //避免不必要的同步,如果已经存在对象
			return instance;
		}
		//防止多个线程 需要加上锁 synchronized
		synchronized (DoubleCheckedLocking.class) {
			if (null == instance) {
				instance = new DoubleCheckedLocking();
				/** //创建对象  可能存在指令重排
				 * 1、开辟空间 2、初始化对象信息 3、返回对象的地址给引用
				 */
			}
		}
		return instance;
	}
	
	public static void main(String[] args) {
		Thread thread = new Thread(()->{
			System.out.println(DoubleCheckedLocking.getInstance());
		});
		thread.start();
		System.out.println(DoubleCheckedLocking.getInstance());
	}
}
com.gqz.thread.DoubleCheckedLocking@816f27d
com.gqz.thread.DoubleCheckedLocking@816f27d

  表明在多线程下,还是得到同一个单例对象。

posted @ 2019-07-10 20:41  gqzdev  阅读(304)  评论(0编辑  收藏  举报