多线程下的单例模式详解

Posted 若曦`

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程下的单例模式详解相关的知识,希望对你有一定的参考价值。

1. 单例模式

(1) 单例模式简介

单例模式的作用

单例模式是为了确保一个类只有一个实例,而且能自行的实例化,并向整个整个系统提供这个实例

单例模式的特点

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

单例模式的使用场景

  • 整个程序的运行中只允许有一个类的实例

  • 需要频繁实例化然后销毁的对象

  • 创建对象时耗时过多或者耗资源过多,但又经常用到的对象

一般是对于那些业务逻辑上限定不能存在多实例的情况

例如:序列号生成器(解决不能id自增的问题),计数器—统计网站访问人数等场景,单例线程池等,都需要使用一个系统唯一实例来进行记录,若多实例计数则会不准确

单例模式的优缺点

优点:只有一个实例,节约了内存资源,提高了系统性能

缺点

  • 没有抽象层,不能扩展
  • 职责过重,违背了单一性原则

(2) 实现方式

① 饿汉式

饿汉式是指在第一次加载类的时候,就实例化对象,也就是在构造函数中创建单例对象

/**
 * @author ruoxi
 */
public class TestSingleTon {
    public static void main(String[] args) {
        SingleTon singleTon1 = SingleTon.getSingleTon();
        SingleTon singleTon2 = SingleTon.getSingleTon();
        System.out.println(singleTon1==singleTon2); //true
    }
}
/**
 * 饿汉式
 */
class SingleTon{
    /**
     * 注意需要使用 static和final修饰 并在这里直接实例化
     */
    private static final SingleTon singleTon = new SingleTon();
    /**
     * 定义private私有构造器,表示只在类内部使用,亦指单例的实例只能在单例类内部创建
     */
    private SingleTon(){}
    /**
     * 返回内部的singleTon实例
     * @return
     */
    public static SingleTon getSingleTon(){
        return singleTon;
    }
    /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
	public Object readResolve() {
		return instance;
	}
}

② 懒汉式

懒汉式就是不在类加载时就创建类的单例,而是在第一次使用实例的时候再创建

/**
 * @author ruoxi
 */
public class TestSingleTon {
    public static void main(String[] args) {
        SingleTon2 singleTon21 = SingleTon2.getSingleTon();
        SingleTon2 singleTon22 = SingleTon2.getSingleTon();
        System.out.println(singleTon21==singleTon22); //true
    }
}

/**
 * 懒汉式
 */
class SingleTon2{
    /**
     * 懒汉式不在此处实例化
     */
    private static SingleTon2 singleTon=null;
    private SingleTon2(){}
    /**
     * 如果singleTon为空则进行实例化
     * @return
     */
    public static SingleTon2 getSingleTon(){
        if(singleTon==null) {
            singleTon = new SingleTon2();
        }
        return singleTon;
    }
    /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
	public Object readResolve() {
		return instance;
	}

}

2. 多线程下的单例模式

对于饿汉式的实现方式,在多线程下也能保证单一实例

但是对于懒汉式来说,在一个线程获取实例的时候,可能会有另一个线程也在获取实例,导致产生两个及以上的实例对象出现

(1) Synchronized

/**
 * synchronized实现多线程的单例模式
 */
class ThreadSingleTon{
    private static ThreadSingleTon singleTon = null;
    private ThreadSingleTon(){}
    /**
     * 使用synchronized防止多个线程同时调用这个方式去创建
     * @return
     */
    public static synchronized ThreadSingleTon getThreadSingleTon(){
        //为空则创建对象
        if(singleTon==null) {
            singleTon = new ThreadSingleTon();
        }
        return singleTon;
    }
}

使用synchronized修饰方法,可以实现多线程下的单例模式

但是每次调用该方法,都会给方法加锁,而只有第一次创建对象的时候需要加锁,其他时候都不需要,这样会导致程序的效率地下,那么可以使用下面的方式(双重检查锁)

(2) 双重检查锁

/**
 * 双重检查锁
 */
class ThreadSingleTon2{
    private static ThreadSingleTon2 singleTon = null;
    private ThreadSingleTon2(){}
    /**
     * 双重检查锁 具体解释看代码注释
     * @return
     */
    public static ThreadSingleTon2 getThreadSingleTon(){
        if(getThreadSingleTon()==null) {
            //如果对象为空,则是第一次实例化,这时锁住对象
            //给ThreadSingleTon.class加锁也可以
            synchronized (singleTon){
                //第二次判断是否为空,防止多线程操作时,在执行第一次判断后另一个线程完成了实例化
                if(singleTon==null){
                    singleTon = new ThreadSingleTon2();
                }
            }
        }
        return singleTon;
    }
}

使用上述方式,但还是会出现意外情况,这和java的线程工作内存有关,我们用下图流程来说明
在这里插入图片描述
也就是当线程1实例化后,还未放入主内存的间隙中,线程2拿到锁开始执行,又创建一个实例化对象

这时可以使用valotile使主内存中的对象对线程可见,来解决上述问题
在这里插入图片描述

(3) 双重检查锁+Volatile

这时就是完美的多线程下的单例模式了,解决了所有可能出现的问题

/**
 * 双重检查锁+volatile
 */
class ThreadSingleTon2{
    /**
     * 使用volatile使主内存中的singleTon对线程可见
     */
    private volatile static ThreadSingleTon2 singleTon = null;
    private ThreadSingleTon2(){}
    /**
     * 双重检查锁 具体解释看代码注释
     * @return
     */
    public static ThreadSingleTon2 getThreadSingleTon(){
        if(getThreadSingleTon()==null) {
            //如果对象为空,则是第一次实例化,这时锁住对象
            //给ThreadSingleTon.class加锁也可以
            synchronized (singleTon){
                //第二次判断是否为空,防止多线程操作时,在执行第一次判断后另一个线程完成了实例化
                if(singleTon==null){
                    singleTon = new ThreadSingleTon2();
                }
            }
        }
        return singleTon;
    }
    /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
	public Object readResolve() {
		return getInstance();
	}
}

以上是关于多线程下的单例模式详解的主要内容,如果未能解决你的问题,请参考以下文章

java-多线程下的单例模式

多线程下的单例模式

三单例模式详解

多线程下的单例模式

多线程下的单例模式

多线程下的单例模式