【一篇博客搞懂:单例模式】

1.单例模式常见问题

1、为什么要有单例模式

  • 单例模式是一种设计模式,它限制了实例化一个对象的行为,始终至多只有一个实例。当只需要一个对象来协调整个系统的操作时,这种模式就非常有用.它描述了如何解决重复出现的设计问题,
    比如我们项目中的配置工具类,日志工具类等等

2、如何设计单例模式 ?

  • 1.单例类如何控制其实例化
  • 2.如何确保只有一个实例
    通过一下措施解决这些问题:
    private构造函数,类的实例话不对外开放,由自己内部来完成这个操作,确保永远不会从类外部实例化类,避免外部随意new出来新的实例。
    该实例通常存储为私有静态变量,提供一个静态方法,返回对实例的引用。如果是在多线程环境下则用锁或者内部类来解决线程安全性问题。

2、单例类的特点

1、私有构造函数

  • 它将阻止从类外部实例化新对象

2、它应该只有一个实例

  • 这是通过在类中提供实例来方法完成的,阻止外部类或子类来创建实例。这是通过在java中使构造函数私有来完成的,这样任何类都不能访问构造函数,因此无法实例化它。

3、单实例应该是全局可访问的

  • 单例类的实例应该是全局可访问的,以便每个类都可以使用它。在Java中,它是通过使实例的访问说明符为public来完成的。

4、节省内存,减少GC

  • 因为是全局至多只有一个实例,避免了到处new对象,造成浪费内存,以及GC,有了单例模式可以避免这些问题

3. 单例模式几种种写法

  • 下面由我给大家介绍8种单例模式的写法,各有千秋,存在即合理,通过自己的使用场景选一款使用即可。我们选择单例模式时的挑选标准或者说评估一种单例模式写法的优劣时通常会根据一下两种因素来衡量:
  • 1.在多线程环境下行为是否线程安全
    2.饿汉以及懒汉
1、饿汉式线程安全的
public class SingleTon{
    
    

	private static final SingleTon INSTANCE = new SingleTon();

	private SingleTon(){
    
     }

	public static SingleTon getInstance(){
    
    
		return INSTANCE;
	}
	public static void main(String[] args) {
    
    
        SingleTon instance1 = SingleTon.getInstance();
        SingleTon instance2 = SingleTon.getInstance();
        System.out.println(instance1 == instance2);
    }

}
  • 这种写法是非常简单实用的,值得推荐,唯一缺点就是懒汉式的,也就是说不管是否需要用到这个方法,当类加载的时候都会生成一个对象。
    除此之外,这种写法是线程安全的。类加载到内存后,就实例化一个单例,JVM保证线程安全
2. 懒汉式线程不安全
public class SingleTon{
    
    

	private static  SingleTon instance ;

	private SingleTon(){
    
    }

	public static SingleTon getInstance(){
    
    
            if(instance == null){
    
    
                instance = new SingleTon();
            }
            return instance;
	}

	public static void main(String[] args) {
    
    
        SingleTon instance1 = SingleTon.getInstance();
        SingleTon instance2 = SingleTon.getInstance();
        System.out.println(instance1 == instance2);
        
        // 通过开启100个线程 比较是否是相同对象
        for(int i=0;i<100;i++){
    
    
             new Thread(()->
                System.out.println(SingleTon.getInstance().hashCode())
            ).start();
        }
        
    }
}
  • 这种写法虽然达到了按需初始化的目的,但却带来线程不安全的问题,至于为什么在并发情况下上述的例子是不安全的呢 ?
 // 通过开启100个线程 比较是否是相同对象
        for(int i=0;i<100;i++){
    
    
             new Thread(()->
                System.out.println(SingleTon.getInstance().hashCode())
            ).start();
        }
  • 为了使效果更直观一点我们对getInstance 方法稍做修改,每个线程进入之后休眠一毫秒,这样做的目的是为了每个线程都尽可能获得cpu时间片去执行。代码如下:
public static SingleTon getInstance(){
    
    
       if(instance == null){
    
    
           try {
    
    
               Thread.sleep(1);
           } catch (InterruptedException e) {
    
    
               e.printStackTrace();
           }
           instance = new SingleTon();
       }
   	return instance;
   }

结果:
在这里插入图片描述
上述的单例写法,我们是可以创造出多个实例的,至于为什么在这里要稍微解释一下,这里涉及了同步问题
造成线程不安全的原因:

  • 当并发访问的时候,第一个调用getInstance方法的线程t1,在判断完singleton是null的时候,线程A就进入了if块准备创造实例,但是同时另外一个线程B在线程A还未创造出实例之前,就又进行了singleton是否为null的判断,这时singleton依然为null,所以线程B也会进入if块去创造实例,这时问题就出来了,有两个线程都进入了if块去创造实例,结果就造成单例模式并非单例。
:这里通过休眠一毫秒来模拟线程挂起,为初始化完instance

在这里插入图片描述
为了解决这个问题,我们可以采取加锁措施,所以有了下面这种写法

3.懒汉式线程安全(粗粒度Synchronized)
public class SingleTon{
    
    

	private static  SingleTon instance ;

	private SingleTon(){
    
    }

	public static SingleTon synchronized getInstance(){
    
    
	    if(instance == null){
    
    
                instance = new SingleTon();
	    }
	    return instance;
	}

	public static void main(String[] args) {
    
    
	    SingleTon instance1 = SingleTon.getInstance();
	    SingleTon instance2 = SingleTon.getInstance();
	    System.out.println(instance1 == instance2);
            // 通过开启100个线程 比较是否是相同对象
            for(int i=0;i<100;i++){
    
    
                new Thread(()->
                System.out.println(SingleTon.getInstance().hashCode())
            ).start();
        }
        
    }

}
  • 由于第三种方式出现了线程不安全的问题,所以对getInstance方法加了synchronized来保证多线程环境下的线程安全性问题,这种做法虽解决了多线程问题但是效率比较低。
    因为锁住了整个方法,其他进入的现成都只能阻塞等待了,这样会造成很多无谓的等待。

  • 于是可能有人会想到可不可以让锁的粒度更细一点,只锁住相关代码块可否?所以有了下面写法:

4.懒汉式线程安全(双重检验加锁)
public class SingleTon{
    
    

	private static  volatile SingleTon instance ;

	private SingleTon(){
    
    }

	public static SingleTon getInstance(){
    
    
	    if(instance == null){
    
    
	        synchronied(SingleTon.class){
    
    
                    if(instance == null){
    
    
                        instance = new SingleTon();
                    }
	        }
	    }
	    return instance;
	}

	public static void main(String[] args) {
    
    
        SingleTon instance1 = SingleTon.getInstance();
        SingleTon instance2 = SingleTon.getInstance();
        System.out.println(instance1 == instance2);
        
        // 通过开启100个线程 比较是否是相同对象
        for(int i=0;i<100;i++){
    
    
             new Thread(()->
                System.out.println(SingleTon.getInstance().hashCode())
            ).start();
        }
       
    }
}

第一个判空(外层)的作用 ?

  • 首先,思考一下可不可以去掉最外层的判断? 答案是:可以
    其实仔细观察之后会发现最外层的判断跟能否线程安全正确生成单例无关!!!
    它的作用是避免每次进来都要加锁或者等待锁,有了同步代码块之外的判断之后省了很多事,当我们的单例类实例化一个单例之后其他后续的所有请求都没必要在进入同步代码块继续往下执行了,直接返回我们曾生成的实例即可,也就是实例还未创建时才进行同步,否则就直接返回,这样就节省了很多无谓的线程等待时间,所以最外的判断可以认为是对提升性能有帮助

第二个判空(内层)的作用 ?

  • 假设我们去掉同步块中的是否为null的判断,有这样一种情况,A线程和B线程都在同步块外面判断了instance为null,结果t1线程首先获得了线程锁,进入了同步块,然后t1线程会创造一个实例,此时instance已经被赋予了实例,t1线程退出同步块,直接返回了第一个创造的实例,此时t2线程获得线程锁,也进入同步块,此时t1线程其实已经创造好了实例,t2线程正常情况应该直接返回的,但是因为同步块里没有判断是否为null,直接就是一条创建实例的语句,所以t2线程也会创造一个实例返回,此时就造成创造了多个实例的情况。
5.静态内部类的方式
public class SingleTon{
    
    

	public static SingleTon getInstance(){
    
    
	    return StaticSingleTon.instance;
	}
	private static class StaticSingleTon{
    
    
            private static final SingleTon instance = new SingleTon();
	}
	public static void main(String[] args) {
    
    
        SingleTon instance1 = SingleTon.getInstance();
        SingleTon instance2 = SingleTon.getInstance();
        System.out.println(instance1 == instance2);
        
        // 通过开启100个线程 比较是否是相同对象
        for(int i=0;i<100;i++){
    
    
             new Thread(()->
                System.out.println(SingleTon.getInstance().hashCode())
            ).start();
        }
        
    }

}
  • 因为一个类的静态属性只会在第一次加载类时初始化,这是JVM帮我们保证的,所以我们无需担心并发访问的问题。所以在初始化进行一半的时候,别的线程是无法使用的,因为JVM会帮我们强行同步这个过程。
  • 另外由于静态变量只初始化一次,所以singleton仍然是单例的。

猜你喜欢

转载自blog.csdn.net/qq_44682003/article/details/111388554