java 惰性初始化

下面这个可怜的小类实在是太懒了,甚至于都不愿意用通常的方法进行初始化,所以它求助于后台线程。这个程序会打印什么呢?每次你运行它的时候都会打印出相同的东西吗?

public class Lazy {

private static boolean initialized = false;

static {

Thread t = new Thread(new Runnable() {

public void run() {

initialized = true;

}

});

t.start();

try{

t.join();

}catch (InterruptedException e){

throw new AssertionError(e);

}

}

public static void main(String[] args){

System.out.println(initialized);

}

}

虽然有点奇怪,但是这个程序看起来很直观的。静态域initialized初始时被设为false。然后主线程创建了一个后台线程,该线程的run方法将initialized的值设为true。主线程启动了后台线程之后,就调用了join方法等待它的结束。当后台线程完成运行的时候,毫无疑问initialized的值已经被设为了true。当且仅当这个时候,调用了main方法的主线程会打印出initialized的值。如果是这样的话,程序肯定会打印出true吗?如果你运行该程序,你会发现它不会打印任何东西,它只是被挂起了。

为了理解这个程序的行为,我们需要模拟它初始化的细节。当一个线程访问一个类的某个成员的时候,它会去检查这个类是否已经被初始化。在忽略严重错误的情况下,有4种可能的情况:

1.这个类尚未被初始化。

2.这个类正在被当前线程初始化:这是对初始化的递归请求。

3.这个类正在被其他线程而不是当前线程初始化。

4.这个类已经被初始化。

当主线程调用Lazy.main方法时,它会检查Lazy类是否已经被初始化。此时它并没有被初始化(情况1),所以主线程会记录下当前正在进行初始化,并开始对这个类进行初始化。按照我们前面的分析,主线程会将initialized的值设为false,创建并启动一个后台线程,该线程的run方法会将initialized设为true,然后主线程会等待后台线程执行完毕。此时,有趣的事情开始了。

那个后台线程调用了它的run方法。在该线程将Lazy.initialized设为true之前,它也会去检查Lazy类是否已经被初始化。这个时候,这个类正在被另外一个线程进行初始化(情况3)。在这种情况下,当前线程,也就是那个后台线程,会等待Class对象直到初始化完成。遗憾的是,那个正在进行初始化工作的线程,也就是主线程,正在等待着后台线程运行结束。因为这2个线程现在正相互等待着,该程序就死锁了(deadlock)。这就是所有的一切,真是遗憾。

有2种方法可以订正这个程序。到目前为止,最好的方法就是不要在类进行初始化的时候启动任何后台线程:有些时候,2个线程并不比1个线程好。更一般的讲,要让类的初始化尽可能地简单。订正这个程序的第2种方法就是让主线程在等待后台线程之前就完成类的初始化:

//Bad way to eliminate the deadlock. Complex and error prone

public class Lazy {

private static boolean initialized = false;

private static Thread t = new Thread(new Runnable() {

public void run() {

initialized = true;

}

});

static {

t.start();

}

public static void main(String[] args){

try{

t.join();

}catch (InterruptedException e){

throw new AssertionError(e);

}

System.out.println(initialized);

}

}

虽然这么做确实消除了死锁,但是它却是一个非常不好的想法。主线程需要等待后台线程完成工作,但是其他的线程不需要这么做。一旦主线程完成了对Lazy类的初始化,其他线程就可以使用这个类了。这使得在initialized的值还是false的时候,其他线程就可以观察到它。

总之,在类的初始化期间等待某个后台线程很可能会造成死锁。要让类初始化的动作序列尽可能地简单。类的自动初始化被公认为是语言设计上的难题Java的设计者们在这个方面做得很不错。如果你写了一些复杂的类初始化代码,很多种情况下,你这是在搬起石头砸自己的脚。

检查再运行的常见用法是惰性初始化(lazy initialization).惰性初始化的目的是延迟对象的初始化,直到程序真正使用它,同事确保它只初始化一次。


单例模式,不多说了,这个模式和静态函数有什么区别,之前解释过了,不是今天重点。
这个贴的重点是,单例模式的这个单例,如何初始化。
其实单例的初始化有两种:一种是提前初始化,就是说,在程序使用单例时,先由其他程序保证这个实例已经初始化结束。
这个比较简单,也不是本贴的重点。
另外一种就是程序一开始不初始化,仅仅在单例第一次被调用的时候被初始化。
中间怎么演变就不说了。总之这里技巧是要解决一个矛盾:第一次使用时初始化,就必需要用同步机制,在对象使用前是否已经初始化。
但是加锁后,极大的降低了程序的性能,尤其判断比较复杂的时候。曾经有文章在设计模式界批判“单例是魔鬼”就出于此。
现在很多的补充做法可以尽量的避免这种方式。文字不好解释,给个代码模型吧。
public class Singl{
   /**
   *单例形成,不允许外部实例化
   */
   private Singl(){
   }
   
   private static Singl instance;
   
   private Object lock = new Object();
   private static Singl getSingl(){
    if (instance == null){//第一次判断不要同步
    synchronized(lock){//第二次在上锁同步,且要再判断一次
    if (instance == null){//第一次判断不要同步
    instance = new Singl()
    }
    }
    }
    return instance;
   }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值