深入理解线程安全的单例模式

1、单例模式是一种常用的设计模式,定义该对象的类只能允许一个实例存在。

为什么需要单例模式?在有的时候,系统需要全局唯一的对象,例如系统时间、某些统一配置数据,

只有单例模式才能正常的管理和应用这些数据;

2、常见的创建单例模式的写法,先私有构造器,然后提供 public static getInstance()方法 返回该对象实例,

例如java.util.Calendar 中的日期时间类:

 Calendar now = Calendar.getInstance();

还有Runtime类的 getRunTime()方法(这里命名就不是getInstance()方法了):


public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
}

3、那么哪些单例模式才是线程安全的呢?

在现在的大多多线程并发环境中,我们需要考虑到线程安全的获取单例模式做法;

先来看一下饿汉模式Singleton.getInstance(),其实上面的Runtime.getRunTime()也是饿汉模式,这种是线程安全的,哪怕有多个线程同时执行Singleton.getInstance()时,其实在调用这个方法之前,发生并发之前,JVM虚拟机已经为我们创建好了这个单例对象,类在整个生命周期中只会被加载一次,所以是线程安全的

(类加载初始化的时候,先执行静态变量,这一句:private final static Singleton INSTANCE = new Singleton();

再执行静态代码块如果有静态代码块,然后再执行构造方法创建对象,如果想了解类初始化时的执行顺序,可以看看这篇博客:

https://www.baidu.com/link?url=nw4zhbDHKVVMApmBoizOwGgQ2-u5ANAgtlI1Y3S9r07I05oKoiscckDWhnXVqv27eKc0BqLxIEVXuWQHZG__ndDJC3Z6KDwJiTwnTQt3NR7&wd=&eqid=df70c112001cfcb4000000035ed3b2c3):

public class Singleton {

    private final static Singleton INSTANCE = new Singleton();

    private Singleton(){}

    public static Singleton getInstance(){
        return INSTANCE;
    }
}

4、饿汉模式的写法保证了线程安全,但是这样也有他的缺点,如果这个对象从头到尾一直没有用到,那么就会一直占用内存空间,引起不必要的浪费;


那么有没有延迟加载方式的单例模式呢?如下懒汉模式:

public class Singleton {

    private static Singleton singleton;

    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

5、上面写法是线程不安全的,所以有时候说加上synchronized 关键字,保证了线程安全,但是效率很低,目前每获取一次都要同步等待,所以不推荐:

public class Singleton {

    private static Singleton singleton;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

6、于是又诞生了一种双重检查模式,这种方式是线程安全的吗?

public class Singleton {
    private static Singleton singleton;// 第二行
    private Singleton() {}
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton(); //第八行
                }
            }
        }
        return singleton;
    }
}

(第八行这一代码 instance=new Singleton())创建了一个对象。这一 行代码可以分解为如下的3行伪代码。

memory = allocate(); // 1:分配对象的内存空间

ctorInstance(memory); // 2:初始化对象

instance = memory; // 3:设置instance指向刚分配的内存地址

上面3行伪代码中的2和3之间,可能会被重排序(在一些JIT编译器上,这种重排序是真实 发生的,2和3之间重排序之后的执行时序如 下。

memory = allocate(); // 1:分配对象的内存空间

instance = memory; // 3:设置instance指向刚分配的内存地址 // 注意,此时对象还没有被初始化!

ctorInstance(memory); // 2:初始化对象

所以这样,在多个线程访问情况下,可能造成创建了不只一个对象,达不到线程安全的效果;

解决方案:在第二行代码上加一个 volatile 关键字, private static volatile Singleton singleton,达到禁止指令重排序,线程安全效果;这一点再具体的分析,在读《Java并发编程的艺术》一书里面,讲的很清楚,推荐看一下:

《Java并发编程的艺术》 读书笔记 之 Java内存模型(八)双重检查锁定与延迟初始化

7、除了用双重模式实现延迟加载单例模式以外,还有一种,静态内部类的方式实现:

public class Singleton {

	private Singleton() {
	}

	private static class SingletonInstance {
		private static final Singleton INSTANCE = new Singleton();
	}

	public static Singleton getInstance() {
		return SingletonInstance.INSTANCE;
	}
}

采用了类装载的机制来保证初始化实例时只有一个线程,类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的;

8、还有另外一种新方式,来实现单例模式,枚举类:

在读《Effective Java》的时候,提到有一种单例模式:

public class Singleton {

  private Singleton(){ }

  //定义一个静态枚举类
  static enum SingletonEnum{
    //创建一个枚举对象,该对象为单例
    INSTANCE;
    private Singleton instance;

    private SingletonEnum(){
      instance=new Singleton();
    }
    public Singleton getInstnceEnum(){
      return instance;
    }
  }

  //对外的静态方法,该方法获取单例对象
  public static Singleton getInstance(){
    return SingletonEnum.INSTANCE.getInstnceEnum();
  }
}
原因有下二:
1、反射的时候可以创建多个单例对象:
《Effective java》中只简单的提了几句话:
享有特权的客户端可以借助AccessibleObject.setAccessible方法,
通过反射机制调用私有构造器。
如果需要抵御这种攻击,可以修改构造器,
让它在被要求创建第二个实例的时候抛出异常。

2、序列化反序列化的时候,也会创建多出来的对象:
如果避免这种情况创建多个对象,需要在声明中加上
" implements Serializable" ,
并且把实例域声明为transient变量,
此外还需要提供一个readResolve()方法

看起来有点和静态内部类实现单例的方式相似,但是用这种方式,可以防止反序列化和反射的时重新创建新的对象,所以作者也说这是最佳实现单例的方式:

书中原文截图如下:

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值