Java语言单例模式的实现

原创
2014/03/05 19:34
阅读数 139

单例模式,是设计模式中创建型模式的一种。创建型模式,顾名思义,主要就是用来控制对象的创建过程的嘛。那由单例模式控制的对象创建过程又有什麽特点呢?由它名称中的单例两字,我们也不难猜测到,我们可以通过这种模式,使得某个class在整个虚拟机中就只有一个实例。

第一种,基本的延迟创建方法

我们先来看一种最简单的实现方式:

package com.example.hello;
public class Singleton {
    private static Singleton instance;
    
    private Singleton() { }
    
    public static Singleton getInstance () {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
    
    public void sayHelloToWorld () {
        System.out.print("Hello, world");
    }
}

在这种实现方式下,构造函数被声明为了private,因而要想访问Singleton类的对象,则绝对是只能通过Singleton.getInstance()来完成的,通过Singleton.getInstance()来控制对象的创建过程,也就可以保证,Singleton类的对象只能被创建一次。看上去似乎真的是实现的挺不错了。不过,实际上呢?实际上就是,在并发环境中,这种实现方式就会显得异常的脆弱。

这种实现方式会发生竟态条件。通常由更底层的观点来看,修改一个变量的过程,大致可以认为由几个步骤组成,从内存中读取变量到寄存器,修改变量的值,然后将变量值写回到内存。在这几个步骤中的任何两个步骤之间,正在执行的线程都有可能正好遇到时间片耗尽,而退出执行的情况。

就这个case而言,静态则主要发生在instance的检查,对象创建,及赋值的过程。假设第一个线程ThreadA先调用Singleton.getInstance(),它发现instance为null,于是它去创建对象,然后给instance赋值,在创建对象或者赋值完成之前的某个时刻,它的时间片用完了,于是退出执行;而此时另外一个线程ThreadB又调用了Singleton.getInstance(),它将同样发现instance为null,从而将无法保障对象在VM中的唯一性。

第二种,同步的延迟创建方法

package com.example.hello;
public class Singleton {
    private static Singleton instance;
    
    private Singleton() { }
    
    public static synchronized Singleton getInstance () {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
    
    public synchronized void sayHelloToWorld () {
        System.out.print("Hello, world");
    }
}

这种方法,相对于前一种方法而言,基本上就是只将各个方法声明为synchronized的了。但这一点小小的改动,却可以很好的保证线程安全性。有人说,Singleton.getInstance()在大多数情况下其实都是没有必要加锁的,而直接将方法声明为synchronized,则加锁操作的开销会有点大。但加锁可以分为竞争加锁和非竞争加锁,竞争加锁,就是在某一时刻,多个线程同时去抢一把锁,而非竞争加锁,则是在某个时刻,就只有一个线程去抢那把锁。除了那些老古董,在现代的JVM上,大多数情况下将会出现的非竞争加锁将是非常迅速的,大概就只有几个或者十几个时钟周期而已,因而,几乎就不需要担心在此处的这种加锁的开销。

同时,这种方法也是很多Java并发大牛推荐的方法。

第三种,双重检查加锁方法

package com.example.hello;
public class Singleton {
    private static volatile Singleton instance;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
    public synchronized void sayHelloToWorld() {
        System.out.print("Hello, world");
    }
}

这种方法相对于前一种方法而言,可以稍微降低一点加锁的开销。但是要注意instance是被声明为volatile的。如果不声明为volatile,则可能会出现可见性问题。JVM提供的保证是,那段code在单任务环境下和多任务环境下执行,最终结果是一样的,而不会去管JVM是否会采用一些类似于乱序执行之类的优化措施。所以赋值语句instance = new Singleton(),在更微观的层面来看,则有可能在对象还没有完全创建结束的时候,instance就已经指向对象的内存块了。因此,其他线程有可能会看到一个还没有完全创建好的对象;而将instance声明为volatile则可以避免这样的问题。

不过这种模式实现起来就要复杂一些。

第四种,无延迟创建(提前初始化)的方法

package com.example.hello;
public class Singleton {
    private static Singleton instance = new Singleton();;
    
    private Singleton() { }
    
    public static Singleton getInstance () {
        return instance;
    }
    
    public synchronized void sayHelloToWorld () {
        System.out.print("Hello, world");
    }
}

另外一种可以避免多线程问题的方法。这种方法借助于classloader的机制,通过静态创建的方法,在class被load起来的时候,就将对象创建好,从而使得,在后面调用Singleton.getInstance()时,不需要有任何的加锁开销。但这种方法的缺点也是显而易见。class可能会由于各种原因被load起来,但instance确是只有在调用Singleton.getInstance()之后才有必要创建的。对于那种很大的Singleton对象,这种方法可能会消耗过多本不必要消耗的内存、处理器时间等资源。

第五种,无延迟创建(提前初始化)的方法2

package com.example.hello;
public class Singleton {
    private static Singleton instance;
    static {
        instance = new Singleton();
    }
    private Singleton() {
    }
    public static Singleton getInstance() {
        return instance;
    }
    public synchronized void sayHelloToWorld() {
        System.out.print("Hello, world");
    }
}

这种方法和前面的第四种方法可以说是一模一样的。

第六种,静态内部类方法

package com.example.hello;
public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton() {
    }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    public synchronized void sayHelloToWorld() {
        System.out.print("Hello, world");
    }
}

这种方法,是将前面的提前初始化技术和JVM的延迟加载机制结合起来,而形成的一种在常见代码路径中不需要加锁而同时又可以延迟加载的技术。它使用一个专门的类SingletonHolder来初始化对象。JVM将推迟这个类的初始化操作,直到开始使用这个类时才初始化,并由于通过一个静态初始化来初始化Singleton,而不需要额外的同步。当任何一个线程第一次调用getInstance()时,都会使SingletonHolder被加载和被初始化,此时静态初始化器将执行Singleton的初始化


Done。

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
9 收藏
0
分享
返回顶部
顶部