多线程下的单例模式详解

  • 1. 单例模式
    • (1) 单例模式简介
    • (2) 实现方式
      • ① 饿汉式
      • ② 懒汉式
  • 2. 多线程下的单例模式
    • (1) Synchronized
    • (2) 双重检查锁
    • (3) 双重检查锁+Volatile
  • 补充知识点

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 this.singleTon;
	}
}

② 懒汉式

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

/**
 * @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 this.singleTon;
	}

}

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张图片

也就是当线程1实例化后,还未放入主内存的间隙中,线程2拿到锁开始执行,又创建一个实例化对象

这时可以使用valotile使主内存中的对象对线程可见,来解决上述问题
多线程下的单例模式详解_第2张图片

(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 this.singleTon;
	}
}

补充知识点

为什么单例模式的内部类需要使用private static final修饰?

  • private :不能让外部能直接操作该类属性;
  • static :确保内部类是属于该类的,而不是类的实例化对象的,否则一个实例化对象就会有一个内部类,也就不是单例了;
  • final :一方面防止指令重排出现问题,还有就是防止使用反射去操作内部类去生成新的对象,以确保单例模式的安全性;

你可能感兴趣的:(多线程,设计模式,设计模式,java,多线程,单例模式)