设计模式浅谈 —— 单例模式

2016-11-06  本文已影响76人  tanghuailong

作者 tanghuailong

如果喜欢那就去做吧

单例模式

初探单例
实现的分类

单例模式的实现,其实总共就分为两种,一种为懒汉式,一种饿汉式。

提起单例模式的分类我又想起一个故事。今年初入职场面试的时候,面试的问我,你知道单例模式的懒汉模式和饿汉模式么。我当时的内心是,WTF? ,啥是懒汉?啥是饿汉。。。
后来才去查了一下,奥,这东西就叫懒汉呀,这特么不是懒加载么!

public class Singleton {
    //实例化singleton
    private static final Singleton instance = new Singleton();
   //定义为私有,确保外部不能使用此构造方法
    private Singleton() {}

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

上面的代码就是饿汉式的实现,在加载类的时候,就进行了实例化,如果 private singleton(){} 里面包含比较笨重的操作时候,使用这种方式,会在一开始启动的时候,比较浪费时间,带来一种不好的体验。另外代码里需要注意的点,我给加上了注释。

public class Singleton {
  //一开始并未进行 实例化,实例化放在调用 getInstance()时候才实例化
    private static volatile Singleton instance = null;

    private Singleton() {   }

    public static Singleton getInstance() {
        // A 点
        if (instance == null) {
       //B 点
            synchronized (Singleton.class){
  //C 点
                if (instance == null) {
                    instance = new Singleton();
                }
                // 错误写法(错误)
                // instance = new Singleton();
            }
        }
        return instance;
    }
}

说一下 双重验证加锁 ,这个名字是我自己根据英文起的,记不起叫什么名字了,这里把需要注意的点说一下。
为什么要在getInstance()方法中判断两次是非为null?
答: 因为在多线程的情况下,进入A 点的可能是多个线程,即进入getInstance()方法的可能是多个线程,当进入B点的时候,可能是线程1先到了B点,之后休眠,线程二也进入了B点。所以在B点的时候也有可能是多个线程。紧接着 到达了 scnchronized块中,因为同步块里只允许一个线程,所以当线程1运行完了之后,线程2才进入,如果用下面的错误写法,会导致线程2进入的时候也会实例化一个对象,这就和我们要求的目的不一样了,所以还要进行一个null判断。
关键词volatile起到的作用?
答:
1.首先应该明确关键词volitile在java中的作用

可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值

用volatile来包装变量来实现对多个线程的可见性,即线程1修改了变量,线程2中的变量值随之也发送变化。如果不使用volatile,当线程1执行完毕之后,线程2进入C点,因为没有使用volatile所以,这时候线程2中instance还有可能为null

2.禁止重排优化,关于这点可以参考这篇博客,写的非常好。
注意 这种单例模式的实现方式,并不是一个优秀的方式,太复杂了

class Singleton{
    private static class SingletonHolder{
        static final Singleton instance = new Singleton();
    }
    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}

关于内部类实现单例模式,有以下几点需要注意的
内部类 方式是如何实现懒加载?
答: 当它真正被用到的时候之前,即调用getInstance(), static class 不会被VM 加载,其实不光 static class ,任何的 Class 都不会被加载。参考 See the JLS -12.4.1 When Initialization Occurs
内部类方式是如何做到线程安全的?
答: 第一个线程调用 getInstance()的时候,JVM 会保持这个内部类,即singtonholder ,当第二个线程也在同时调用getInstance(),JVM 会等到第一个线程完成加载之后才会让给第二个线程访问。所以第二个线程得到的是已经加载完成的singletonholder。并且 JLS 规则保证每个类只会被第一次用到的时候加载一次。
参考 singleton-pattern-bill-pughs-solution

public enum Sington {
// 只会有一个INSTANCE 实例
    INSTANCE;
    public void foo() {
        //to do someing
    }
}
//调用方式,在下面这样访问的情况下才会加载。
Sington.INSTANCE.foo();

其实enum的实现方式,enum里面的字段属性为 public static final,另外enum方式其实算伪懒加载吧,应该放在饿汉模式里面的。至于原因,我下面将会做出解释。

关于饿汉和懒汉的思考

在说到这点的时候,首先你确认你知道类是在什么时候加载的么?

当类第一次被用到的时候,才会被JVM加载,并且初始化所有static 变量。注意这里是说的是加载,并不是实例化。
Object object = new Object(),类似于这样叫实例化

正如下面这个例子。

public class LazyEnumTest {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("Sleeping for 5 seconds...");
        Thread.sleep(5000);
        System.out.println("Accessing enum...");
        LazySingleton lazy = LazySingleton.INSTANCE;
        System.out.println("Done.");
    }
}
//测试一
enum LazySingleton {
    INSTANCE;
    static { System.out.println("Static Initializer"); }
}
// 测试二
public class Singleton {
  //private static final String testStr = "test";
 //private static final int testNum = 0;
 //private static final Object testObj = new Object();
    private static final Singleton instance = new Singleton();
    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
    
   public static void foo(){
    //to do some thing
  }
}

下面是运行结果

$ java LazyEnumTest
Sleeping for 5 seconds...
Accessing enum...
Static InitializerDone.

所以只有在访问 LazySingleton.INSTANCE才会被加载,所以Enum也会被当作懒加载。
到了这个时候你也许会说,那测试二,即饿汉模式岂不是也是懒加载了。对的,你说的没错,的确是懒加载。即使你进行下面的操作,它也是懒加载的。

public class LazyEnumTest {
//此时,并未Singleton类并未加载
    private static final Singleton singleton= null;
    public static void main(String[] args) throws InterruptedException {
//在这个时候Singleton类才加载
      singleton = Singleton.getInstance();
    }
}

所以饿汉模式和懒汉模式其实差不多,对吧,都是懒加载了,那为什么还需要懒汉模式那。
区别在上面我举的例子上。因为作为一个单例模式,你可能依赖一下其他的东西,才能在 private Singleton() {} 完成初始化,比如你需要上面的testObj

当你在一个地方使用如下的写法

//这个时候,类就要进行加载。这时候,我只是要打印一下这个testObj,
//但是这个类就已经加载。这并不是我期望的效果,这个时候就该使用懒汉模式的
System.out.println(Singleton.testObj);
// 或者调用foo()也会一样的效果
Sington.foo();
// 例外的情况,下面的调用,并不会引起类的加载
System.out.println(Singleton.testStr);
System.out.println(Singleton.testNum)

上面例外的原因如下

The use or assignment of a static field declared by a class or interface, except for static fields that are final and initialized by a compile-time constant expression (in bytecodes, the execution of a getstatic or putstatic instruction)
ps ---- it only applies to "static fields that are final and initialized by a compile-time constant expression":

大意就是,当 类或者接口里面的变量属性,为 staic final 时候,并且会被 编译期 常量初始化,该属性才会被加载。但这种操作不会引起类的加载

private static final String testStr = "test";  //compile time constant
 private static final int testNum = 0; //compile time constant
private static final Object testObj = new Object(); //initialised at runtime

参考 Singleton via enum way is lazy initialized?

总结

所以以后使用 单例模式,最好的应该 内部类的方式,但如果你进行初始化的时候,不需要依赖一些其他的东西,那用Enum方式是最好的选择,否则就是内部类的方式。此外Enum还保证了序列化的时候也只产生一个实例。

至于 单例模式和序列化就是另一个故事额。
好累,不想讲了。想了解的请点击这里。。。。单例模式和序列化

上一篇下一篇

猜你喜欢

热点阅读