java lazy 变量_深刻Java单例模式

在GoF的23种设计模式中,单例模式是比较简单的一种。然而,有时候越是简单的东西越容易出现问题。下面就单例设计模式详细的探讨一下。

所谓单例模式,简单来讲,就是在整个应用中保证只有一个类的实例存在。就像是Java Web中的application,也就是提供了一个全局变量,用处至关普遍,好比保存全局数据,实现全局性的操做等。

1. 最简单的实现

首先,可以想到的最简单的实现是,把类的构造函数写成private的,从而保证别的类不能实例化此类,而后在类中提供一个静态的实例并可以返回给使用者。这样,使用者就能够经过这个引用使用到这个类的实例了。

public

class SingletonClass {

private

static

final SingletonClass instance =

new SingletonClass();

public

static SingletonClass getInstance() {

return instance;

}

private SingletonClass() {

}

}

如上例,外部使用者若是须要使用SingletonClass的实例,只能经过getInstance()方法,而且它的构造方法是private的,这样就保证了只能有一个对象存在。

2. 性能优化——lazy loaded

上面的代码虽然简单,可是有一个问题——不管这个类是否被使用,都会建立一个instance对象。若是这个建立过程很耗时,好比须要链接10000次数据库(夸张了…:-)),而且这个类还并不必定会被使用,那么这个建立过程就是无用的。怎么办呢?

为了解决这个问题,咱们想到了新的解决方案:

public

class SingletonClass {

private

static SingletonClass instance =

null;

public

static SingletonClass getInstance() {

if(instance ==

null) {

instance =

new SingletonClass();

}

return instance;

}

private SingletonClass() {

}

}

代码的变化有两处——首先,把instance初始化为null,直到第一次使用的时候经过判断是否为null来建立对象。由于建立过程不在声明处,因此那个final的修饰必须去掉。

咱们来想象一下这个过程。要使用SingletonClass,调用getInstance()方法。第一次的时候发现instance是null,而后就新建一个对象,返回出去;第二次再使用的时候,由于这个instance是static的,因此已经不是null了,所以不会再建立对象,直接将其返回。

这个过程就成为lazy loaded,也就是迟加载——直到使用的时候才进行加载。

3. 同步

上面的代码很清楚,也很简单。然而就像那句名言:“80%的错误都是由20%代码优化引发的”。单线程下,这段代码没有什么问题,但是若是是多线程,麻烦就来了。咱们来分析一下:

线程A但愿使用SingletonClass,调用getInstance()方法。由于是第一次调用,A就发现instance是null的,因而它开始建立实例,就在这个时候,CPU发生时间片切换,线程B开始执行,它要使用SingletonClass,调用getInstance()方法,一样检测到instance是null——注意,这是在A检测完以后切换的,也就是说A并无来得及建立对象——所以B开始建立。B建立完成后,切换到A继续执行,由于它已经检测完了,因此A不会再检测一遍,它会直接建立对象。这样,线程A和B各自拥有一个SingletonClass的对象——单例失败!

解决的方法也很简单,那就是加锁:

public

class SingletonClass {

private

static SingletonClass instance =

null;

public

synchronized

static SingletonClass getInstance() {

if(instance ==

null) {

instance =

new SingletonClass();

}

return instance;

}

private SingletonClass() {

}

}

是要getInstance()加上同步锁,一个线程必须等待另一个线程建立完成后才能使用这个方法,这就保证了单例的惟一性。

4. 又是性能

上面的代码又是很清楚很简单的,然而,简单的东西每每不够理想。这段代码毫无疑问存在性能的问题——synchronized修饰的同步块但是要比通常的代码段慢上几倍的!若是存在不少次getInstance()的调用,那性能问题就不得不考虑了!

让咱们来分析一下,到底是整个方法都必须加锁,仍是仅仅其中某一句加锁就足够了?咱们为何要加锁呢?分析一下出现lazy loaded的那种情形的缘由。缘由就是检测null的操做和建立对象的操做分离了。若是这两个操做可以原子地进行,那么单例就已经保证了。因而,咱们开始修改代码:

public

class SingletonClass {

private

static SingletonClass instance =

null;

public

static SingletonClass getInstance() {

synchronized (SingletonClass.

class) {

if(instance ==

null) {

instance =

new SingletonClass();

}

}

return instance;

}

private SingletonClass() {

}

}

首先去掉getInstance()的同步操做,而后把同步锁加载if语句上。可是这样的修改起不到任何做用:由于每次调用getInstance()的时候必然要同步,性能问题仍是存在。若是……若是咱们事先判断一下是否是为null再去同步呢?

public

class SingletonClass {

private

static SingletonClass instance =

null;

public

static SingletonClass getInstance() {

if (instance ==

null) {

synchronized (SingletonClass.

class) {

if (instance ==

null) {

instance =

new SingletonClass();

}

}

}

return instance;

}

private SingletonClass() {

}

}

还有问题吗?首先判断instance是否是为null,若是为null,加锁初始化;若是不为null,直接返回instance。

这就是double-checked locking设计实现单例模式。到此为止,一切都很完美。咱们用一种很聪明的方式实现了单例模式。

5. 从源头检查

下面咱们开始说编译原理。所谓编译,就是把源代码“翻译”成目标代码——大多数是指机器代码——的过程。针对Java,它的目标代码不是本地机器代码,而是虚拟机代码。编译原理里面有一个很重要的内容是编译器优化。所谓编译器优化是指,在不改变原来语义的状况下,经过调整语句顺序,来让程序运行的更快。这个过程成为reorder。

要知道,JVM只是一个标准,并非实现。JVM中并无规定有关编译器优化的内容,也就是说,JVM实现能够自由的进行编译器优化。

下面来想一下,建立一个变量须要哪些步骤呢?一个是申请一块内存,调用构造方法进行初始化操做,另外一个是分配一个指针指向这块内存。这两个操做谁在前谁在后呢?JVM规范并无规定。那么就存在这么一种状况,JVM是先开辟出一块内存,而后把指针指向这块内存,最后调用构造方法进行初始化。

下面咱们来考虑这么一种状况:线程A开始建立SingletonClass的实例,此时线程B调用了getInstance()方法,首先判断instance是否为null。按照咱们上面所说的内存模型,A已经把instance指向了那块内存,只是尚未调用构造方法,所以B检测到instance不为null,因而直接把instance返回了——问题出现了,尽管instance不为null,但它并无构造完成,就像一套房子已经给了你钥匙,但你并不能住进去,由于里面尚未收拾。此时,若是B在A将instance构造完成以前就是用了这个实例,程序就会出现错误了!

因而,咱们想到了下面的代码:

public

class SingletonClass {

private

static SingletonClass instance =

null;

public

static SingletonClass getInstance() {

if (instance ==

null) {

SingletonClass sc;

synchronized (SingletonClass.

class) {

sc = instance;

if (sc ==

null) {

synchronized (SingletonClass.

class) {

if(sc ==

null) {

sc =

new SingletonClass();

}

}

instance = sc;

}

}

}

return instance;

}

private SingletonClass() {

}

}

咱们在第一个同步块里面建立一个临时变量,而后使用这个临时变量进行对象的建立,而且在最后把instance指针临时变量的内存空间。写出这种代码基于如下思想,即synchronized会起到一个代码屏蔽的做用,同步块里面的代码和外部的代码没有联系。所以,在外部的同步块里面对临时变量sc进行操做并不影响instance,因此外部类在instance=sc;以前检测instance的时候,结果instance依然是null。

不过,这种想法彻底是

错误的!同步块的释放保证在此以前——也就是同步块里面——的操做必须完成,可是并不保证同步块以后的操做不能因编译器优化而调换到同步块结束以前进行。所以,编译器彻底能够把instance=sc;这句移到内部同步块里面执行。这样,程序又是错误的了!

6. 解决方案

说了这么多,难道单例没有办法在Java中实现吗?其实否则!

在JDK 5以后,Java使用了新的内存模型。volatile关键字有了明确的语义——在JDK1.5以前,volatile是个关键字,可是并无明确的规定其用途——被volatile修饰的写变量不能和以前的读写代码调整,读变量不能和以后的读写代码调整!所以,只要咱们简单的把instance加上volatile关键字就能够了。

public

class SingletonClass {

private

volatile

static SingletonClass instance =

null;

public

static SingletonClass getInstance() {

if (instance ==

null) {

synchronized (SingletonClass.

class) {

if(instance ==

null) {

instance =

new SingletonClass();

}

}

}

return instance;

}

private SingletonClass() {

}

}

然而,这只是JDK1.5以后的Java的解决方案,那以前版本呢?其实,还有另外的一种解决方案,并不会受到Java版本的影响:

public

class SingletonClass {

private

static

class SingletonClassInstance {

private

static

final SingletonClass instance =

new SingletonClass();

}

public

static SingletonClass getInstance() {

return SingletonClassInstance.instance;

}

private SingletonClass() {

}

}

在这一版本的单例模式实现代码中,咱们使用了Java的静态内部类。这一技术是被JVM明确说明了的,所以不存在任何二义性。在这段代码中,由于SingletonClass没有static的属性,所以并不会被初始化。直到调用getInstance()的时候,会首先加载SingletonClassInstance类,这个类有一个static的SingletonClass实例,所以须要调用SingletonClass的构造方法,而后getInstance()将把这个内部类的instance返回给使用者。因为这个instance是static的,所以并不会构造屡次。

因为SingletonClassInstance是私有静态内部类,因此不会被其余类知道,一样,static语义也要求不会有多个实例存在。而且,JSL规范定义,类的构造必须是原子性的,非并发的,所以不须要加同步块。一样,因为这个构造是并发的,因此getInstance()也并不须要加同步。

至此,咱们完整的了解了单例模式在Java语言中的时候,提出了两种解决方案。我的偏向于第二种,而且Effiective Java也推荐的这种方式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值