模式的秘密——单例模式

来源:互联网 发布:淘宝自定义轮播代码 编辑:程序博客网 时间:2024/05/14 14:34

模式的秘密——单例模式

            单例模式是指有些对象只需要一个,比如:古代的皇帝,单例的作用是保证整个应用程序中某一个实例有且只有一个。单例模式分为两种类型:饿汉模式和懒汉模式。

           

饿汉模式:当类被加载的时候就会创建类的实例。

            事例源码:

public class Singleton {

     private Singleton() {

     }

     private static Singletoninstance =new Singleton();

     public static SingletongetInstance() {

          returninstance;

     }

}

           

            懒汉模式:创建对象的时候才实例化

            事例源码:

public class Singleton2 {

     private Singleton2() {

     }

     private staticSingleton2 instance;

     public staticSingleton2 getInstance() {

          if (instance ==null) {

               instance =new Singleton2();

          }

          returninstance;

     }

}

饿汉模式和懒汉模式的区别:饿汉模式加载类时比较慢,但运行时获取对象的速度比较快,线程安全;懒汉模式加载类时比较快,但运行时获取对象的速度比较慢,线程不安全。

            我不从饿汉模式还是懒汉模式的角度去细讲,我想从线程安全的角度去优化改进单例模式,所以抛开饿汉模式和懒汉模式,还是先简单讲讲单例模式的特点,先给出一个简单版本的单例模式,姑且叫做1.0版本

            public class Singleton {

     private Singleton() {

     }

     private static Singletoninstance;

     public static SingletongetInstance() {

          if (instance ==null) {

               instance =new Singleton();

          }

          returninstance;

     }

}

从上面的实例中,我想说明下面几个单例模式的特点:

1、私有(private)的构造方法,表明这个类是不能够用来直接创建实例的,这样就能够保证这个类不会有多个实例。

2、声明类的唯一实例instance,使用private static修饰

3、一个类不能够用外部创建实例,那么,我们可以从内部来创建实例,所以我们提供一个public static修饰的方法getInstance(),这样就能够保证实例被创建出来了。

4、需要得到这个实例的时候,直接通过Singleton.getInstance()获取

 

上面的这个程序存在比较严重的问题,因为是全局实例,所以在多线程的情况下,所有的全局共享的东西都会变得非常危险。在多线程情况下,如果多个线程同时调用getInstance(),那么,可能会有多个进程同时通过(singleton == null)的条件检查,于是,多个实例就被创建出来了,并且很可能造成内存泄露问题。我们可以通过“线程互斥或同步”的角度对Singleton进行改进,升级版的1.1版,如下所示:

//   version1.1

     private Singleton(){}

     private static Singletonsingleton =null;

     public static SingletongetInstance(){

          if(singleton ==null){

               synchronized (Singleton.class) {

                     singleton =new Singleton();

               }

          }

          returnsingleton;

   }

使用java的synchronized方法,看起来没有问题,其实问题还是存在的,正如前面描述的那样,如果多个线程同时通过(singleton == null)的条件检查,虽然synchronized方法会帮助我们同步所有的线程,把并行线程串行成一个一个去new,结果还是一样的,同样会创建多个实例,那么我们把条件也同步了会怎样,所以Singleton1.2版本产生了,如下所示:

// version1.2

     private Singleton() {

     }

 

     private static Singletonsingleton =null;

 

     public static SingletongetInstance() {

          synchronized (Singleton.class) {

               if (singleton ==null) {

                     singleton =new Singleton();

               }

          }

          returnsingleton;

   }

经过上面的修改,在多线程的情况下应该没有问题了,因为我们同步了所有的线程。不过还是存在一定的问题,我们本来只是想让new这个操作并行就可以了,现在,只要是进入getInstance()的线程都得同步,创建对象的动作只有一次,后面的动作全是读取那个成员变量,这些读取的动作不需要线程同步,这样种做法比较极端,为了一个初始化的创建动作,居然让我们搭上了所有读写操作的性能!那么,在线程同步前还得加一个(singleton == null)的条件判断,如果对象创建了,就不需要线程同步了,Singleton1.3版本诞生了,如下所示:

// version1.3

     private Singleton() {

     }

 

     private static Singletonsingleton =null;

 

     public static SingletongetInstance() {

          if (singleton ==null) {

               synchronized (Singleton.class) {

                     if (singleton ==null) {

                          singleton =new Singleton();

                     }

               }

          }

          returnsingleton;

   }

     瞬间感觉上面的代码变得有些啰嗦和复杂了,不过,这应该算是一个比较不错的版本了,这个版本又叫做“双重检查(Double-Check)”,简单说明一下:

1、第一个条件是说,如果实例创建了,就不需要同步了,直接返回创建的实例就好了,否则,我们就同步线程

2、第二个条件是说,如果被同步的线程中,有一个线程创建了对象,那么别的线程就不用再创建了。

从感觉上来讲,1.3版本算是大功告成了,事实上并不是那样,主要在于singleton = new Singleton()这句,这并不是一个原子操作,事实上在JVM中这句话大概做了下面3件事:

1、给singleton分配内存

2、调用Singleton的构造方法来初始化成员变量,形成实例

3、将singleton对象指向分配的内存空间

但是JVM的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能够保证的,最终的执行顺序额能使1-2-3也可能是1-3-2.如果是后者,则在3执行完毕,2未执行之前,被线程二抢占,这时instance已经是非null了(但却没有初始化),所以线程二会直接返回instance,然后使用,然后顺理成章地报错。

     对此,我们只需要把singleton声明成volatile就可以了,那么Singleton1.4版本产生了。如下所示:

// version1.4

          private Singleton() {

          }

 

          private volatile static Singleton singleton = null;

 

          public static SingletongetInstance() {

               if (singleton ==null) {

                     synchronized (Singleton.class) {

                          if (singleton ==null) {

                               singleton =new Singleton();

                          }

                     }

               }

               returnsingleton;

          }

使用volatile有两个功用:

1)这个变量不会在多个线程中存在复本,直接从内存读取

2)这个关键字会禁止指令重排序优化。也就是说,在volatile变量的赋值操作后面会有一个内存屏障,读操作不会被重排序到内存屏障之前。

但是,这个事情仅在java1.5版本后又用,1.5版本之前使用这个变量也有问题,因为老版本的Java的内存模型有缺陷。

     上面的写法实在是太复杂了,一点儿也不优雅,下面是一种优雅的方式:

     // version1.5

     private Singleton() {

     }

 

     private volatile static Singleton singleton = new Singleton();

 

     public static SingletongetInstance() {

 

          returnsingleton;

     }

因为单例的实例被声明成static和final变量,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。但是,这种做法有一个最大问题就是,当这个类被加载的时候,new Singleton()这句话就会被执行,就算是getInstance()没有被调用,类也会被初始化。于是,这可能与我们想要的行为不一样,比如,我们的类构造方法中,有一些事情可能需要依赖于别的类做一些事情(比如某个配置文件,或是某个被其它类创建的资源),我们希望他能在我们第一次getInstance()时才被真正创建。这样,我们可以控制真正的类创建时刻,而不是把类的创建委托给类装载器。

     我们还得绕一下:

// version1.6

     private Singleton() {

     }

 

     private static class SingletonHolder {

          private static final Singleton INSTANCE = new Singleton();

     }

 

     public static final Singleton getInstance() {

 

          return SingletonHolder.INSTANCE;

     }

上面这种方式,仍然使用JVM本身机制保证了线程安全问题,由于SingletonHolder是私有的,除了getInstance()之外没有办法访问它,因此它只有在getInstance()被调用时才会真正创建,同时读取实例的时候不会进行同步,没有性能缺陷,也不会依赖JDK版本。

     //version1.7

public enum Singleton{

     INSTANCE;

}

     上面的方法使用枚举,通过Singleton.INSTANCE来访问,这样比调用getInstance()简单多了。默认枚举实例的创建是线程安全的,所以不需要担心线程安全的问题。但是在枚举中的其他任何方法的线程安全由程序人员自己负责,还有防止上面的通过反射机制调用私有构造方法。

 

     我们来看看下面的一些反例和一些别的事情的讨论:

一、ClassLoader。类装载器,这是Java动态性的核心。类装载器是用来把类装载进JVM。JVM规范定义了两种类型的类装载器:启动类装载器(bootstrap)和用户自定义转载器(user-defined class loader)。在一个JVM中可能存在多个ClassLoader,每个ClassLoader拥有自己的NameSpace。一个ClassLoader只能拥有一个class对象类型的实例,但是不同的ClassLoader可能拥有相同的class对象实例,这时可能产生致命的问题。如ClassLoaderA装载了类A的类型实例A1,而ClassLoaderB,也装载了类A的对象实例A2.逻辑上讲A1=A2,但是由于A1和A2来自不同的ClassLoader,它们实际上是完全不同的,如果A中定义了一个静态变量c,则c在不同的ClassLoader中的值是不同的。于是,如果我们的Singleton1.3版本面对着多个ClassLoader会这样?多个实例同样会被多个ClassLoader创建出来,当然,这个有些牵强,不过这种情况确实存在。可是,我们怎么可能在我们的Singleton类中操作ClassLoader呢?是的,这根本不可能。这种情况下,我们只能保证多个ClassLoader不会装载同一个Singleton。

二、序列化。如果我们的这个Singleton类是一个关于我们程序的配置信息的类。我们需要它有序列化的功能,那么,当反序列化的时候,我们无法控制别人不多次反序列化。不过,我们可以利用一下Serializable接口的readResolve()方法,比如:

public class Singleton  implementsSerializable{

  .....

  .....

  protected ObjectreadResolve(){

       returngetInstance();

  }

}

三、volatile变量。关于volatile这个关键字所声明的变量可以被看作是一种“程度较轻的同步synchronized”,与synchronized块相比,volatile变量所需的编码较少,并且运行时开销也较少,但是它仅能实现synchronized的部分功能。当然,如前面所述,我们需要的Singleton只是在创建的时候线程同步,而后面的读取不需要同步。所以,volatile变量并不能帮助我们既解决问题,又优化性能。而且,这种变量只能在JDK1.5+版本后才能使用。

四、关于继承。继承与Singleton后的子类也有可能造成多实例问题。不过,因为我们早把Singleton的构造方法声明成了私有的,所以也就杜绝了继承这种事情。


这是我的第一次写博文,有些紧张!哈哈哈,希望小伙伴能够喜欢,如果喜欢的话记得加关注哦!。

特别感谢陈皓,原文请戳这里http://coolshell.cn/articles/265.html

 

3 0