饿汉方式单例到底有多必威:,设计模式之一

这种形式或许我们无从下手,但反编译后就明白枚举的真身了,相当于:

2.实现单例模式的问题

在java中创建一个对象,我们可以通过:new,clone,序列化,反射。上面单例模式的实现我们通过将构造函数私有化使得不能通过new来创建对象,但是其他的手段依然可以,下面举例说明:

  1. 反射
    通过反射我们可以访问类的私有构造函授,测试代码如下(单例代码见上面1):

    public class TestSingleton {
     public static void main(String args[]){
         try {
             Constructor cons = Singleton.class.getDeclaredConstructor();
             cons.setAccessible(true);
             Singleton instance1 = Singleton.getInstance();
             Singleton instance2 = (Singleton)cons.newInstance();
    
             System.out.println("instance1 == instance2 ?"+(instance1 == instance2));
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
     } 
    

    打印的结果如下:
    instance1 == instance2 ? false
    instance1和instance2是不同对象,因此这就破坏了单例模式,网上提供解决反射带来的问题也十分简单,只需要修改构造函数,使得它第二次以及更多次的调用抛出异常,修改构造函数如下:

    private static boolean flag = false;
    public Singleton(){
         if(false == flag){
             flag = true;
         }else{
              throw new Exception(...);
         }
    }
    

    不过java的反射有点没节操,你还是可以修改flag值,我的天。
    在《effective java》里提供一种解决之道,可以无视反射,那就是通过枚举来实现。像下面这样:

    public enum Singleton3 {
     INSTANCE;
    
     public void applaud(){
         System.out.println("haha, go home,reflection!");
     }
     }
    

    没有构造函数了。。。(跟jvm初始化枚举变量的方式有关系,当你再试图通过反射获取构造函数会抛出异常),所以再尝试通过反射获得构造函数,就会抛异常。

  2. 序列化的影响
    不考虑枚举实现单例模式,如果Singleton实现了Serializable接口,那么如果我们将Singleton序列到一个对象中去,在反序列化出来,就会导致不同的实例,请看下面代码:

    public class TestSingleton2 {
    
     public static void main(String []args){
         try {
             Singleton instance = Singleton.getInstance();
    
             //将instance序列化到文件singleton中.
             FileOutputStream fos = new FileOutputStream("singleton");
             ObjectOutputStream oos = new ObjectOutputStream(fos);
    
             oos.writeObject(instance);
    
             //从文件singleton中读出对象
             FileInputStream fis = new FileInputStream("singleton");
             ObjectInputStream ois = new ObjectInputStream(fis);
    
             Singleton instance1 = (Singleton)ois.readObject();
    
             System.out.println("instance == instance1 ? " + (instance == instance1));
    
         } catch (Exception e) {
             e.printStackTrace();
         }
    
     }
     }
    

结果显示instance和instance1为两个实例。

序列化前后产生不同对象,解决方法也很简单,jvm在反序列化时,如果该类实现的下面方法:
private Object readResolve() throw IOException
那么就会调用这个方法返回对象,以替换流中对象。因此可以在这个方法里返回Singleton的instance成员,如下:

 private  Object readResolve() throws ObjectStreamException{
     return instance;
   }

2.2 双重检测

由于懒汉模式是对方法进行加锁的,所以当多个线程同时访问该方法时,效率很低,synchronized修饰的同步方法比一般方法要慢很多。

双重检测:

public class Singleton {

    private static Singleton sInstance;

    private Singleton() {}

    public static Singleton getInstance () {
        if (sInstance == null) {   // 1
            synchronized (Singleton.class) {
                if (sInstance == null) {  // 2
                    sInstance = new Singleton();
                }
            }
        }

        return sInstance;
    }
}

我们来分析下,双重检测的原理,可以看到上面代码1处在同步代码块外多了一层instance为空的判断。由于单例对象只需要创建一次,如果后面再次调用getInstance()只需要直接返回单例对象。因此,大部分情况下,调用getInstance()都不会执行到同步代码块,从而提高了程序性能。不过还需要考虑一种情况,假如两个线程A、B,A执行了if (instance == null)语句,它会认为单例对象没有创建,此时线程切到B也执行了同样的语句,B也认为单例对象没有创建,然后两个线程依次执行同步代码块,并分别创建了一个单例对象。为了解决这个问题,还需要在同步代码块中增加if (instance == null)语句,也就是上面看到的代码2。

  • 这种实现就真的完美么?

由于java平台的指定优化重排后的无序性,会导致初始化Singleton和将对象地址赋给instance字段的顺序是不确定的。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错,双重检测就会失效。

不过JDK 1.5后 Java中提供了关键字 volatile。 volatile的一个语义是禁止指令重排序优化,他定义的变量在多线程操作中,是对所有线程可见的,也就保证了instance变量被赋值的时候对象已经是初始化过的,从而避免了上面说到的问题。

private static volatile Singleton sInstance;    // volatile 关键字的使用

5、枚举

应用中每次启动只会存在一个实例。如账号系统,数据库系统。

public class Singleton { public static final Singleton INSTANCE; static{ INSTANCE = new Singleton(); }}

1. 实现单例模式

  1. 饿汉模式和懒汉模式
    单例模式根据实例化时机分为饿汉模式和懒汉模式。
    饿汉模式,是指不等到单例真正使用时在去创建,而是在类加载或者系统初始化就创建好。
    懒汉模式中单例要等到第一次使用时才创建。

  2. 饿汉模式
    最简单的实现

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

    上面是一种线程安全的实现方式,因为instance是类静态成员,会在类加载并初始化时创建,因此可以保证即便是不同线程也会获得同一份实例(这句话在有些情况下并不正确,比如通过序列化,反射的方式还是能够创建多个实例出来)。

  3. 懒汉模式

    相对于1中在加载的时候就创建,另一种则是在首次使用时创建,比如下面这种方式:

    class Singleton{
         priavte static Singleton instance = null;
         private Singleton(){};
         public static Singleton getInstance(){
             if(null == instance){
                 instance = new Singleton();
             }
    
             return instance;
         }
     }
    

    上面的这种形式,在首次调用getInstance时才会创建单例,但是它有一个问题就是,在多线程的情况下有可能会创建出多个实例化对象出来:比如线程1和线程2同时判断null == instance为true,结果进入下一步两个线程就创建两个instance出来。当然这种方式通过加锁或则使用synchronize关键字的方式就可以避免了。这里不展示对整个getInstance方法加锁的实现,而是展示另一种方式:

    3.1 两次判断,代码如下:

    class Singleton{
         priavte static volatile Singleton instance = null;
         private Singleton(){};
         public static Singleton getInstance(){
             if(null == instance){
              synchronize(Singleton.class){
                 if(null == instance){
                     instance = new Singleton();
                 }
              }
             }
             return instance;
         }
     }
    

    比起对整个getInstance方法加锁,两次判断的方式可以避免一些不必要的加锁开销。

    同时volatile关键字十分必要,多核环境下,多线程分布在多个核上,每个核心拥有各自的cache,读取数据总会尝试从cache读取。那就意味着instance = new Singleton();可能不会立即被运行在其他核心上的线程所知,导致即便instance更新后,其他线程cache中instance依然是null。volatile关键字保存每次更新都会更新到内存,同时保存其他核心上该缓存项失效,需要从内存读取。

    3.2 内部类实现延迟加载
    上面两次判断的方法依然是通过加锁的方式来保证多线程情况下的创建单一实例,回顾1的实现中,保证只有一个实例是通过jvm只初始化一次static类成员这一机制实现的,但是1中在Singleton类加载的时候就会实例化静态成员instance,这可不是我们想要的首次使用创建这一目的。为了达到这一目的,我们可以借助内部类的方式实现,下面是代码实现:

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

    jvm加载Singleton时并不会加载其SingletonHolder,因此instance就不会被早早的创建,直到调用getInstance方法时才回加载SingletonHolder,而instance是其静态成员,jvm保证了它只此一份。

附:关于类的加载时机
「深入理解java虚拟机」一书中有介绍过类什么时候被初始化:

  1. 创建类的实例时
  2. 使用Class.forName时
  3. 访问类的静态成员
  4. 调用类的静态方法
  5. 子类初始化时,父类也会初始化

1、单例模式的作用

单例模式主要用于解决多次调用的地方都是用的同一个实例,避免一个应用中存在多个该对象的实例,常见的实现手段就是不允许外部创建对象,将构造方法私有化,自己提供静态方法将对象的实例暴露出去。

本文总结了五种Java中实现单例的方法,其中前两种都不够完美,双重校验锁和静态内部类的方式可以解决大部分问题,平时工作中使用的最多的也是这两种方式。枚举方式虽然很完美的解决了各种问题,但是这种写法多少让人感觉有些生疏。个人的建议是,在没有特殊需求的情况下,使用第三种和第四种方式实现单例模式。

原文链接

可以看到,枚举方式实现的单例和饿汉方式差不多,延迟加载时机依赖类加载时机。

单例模式作为23种设计模式中最常用的一种,也是面试中的常客,但是很少有人能把每种实现方式的不同讲清楚的,今天我们就来探讨下它。

上面提到的四种实现单例的方式都有共同的缺点:

单例对象如果持有Context,那么很容易引发内存泄漏,此时要注意传递给单例对象的Context最好是Application Context

如果 Singleton 中对外之暴露了 getInstance 方法,那和饿汉方式无异;如果还暴露了其他的静态方法或字段,那相比饿汉方式,可以更精准的实现延迟加载。

3、总结

上面几种单例模式的实现都各有优缺点,适用于不同的场景,不过双重检测和静态内部类能解决工作中大部分的问题,枚举虽然很有特色、很完美,但是工作中用的还是比较少的。我的建议是使用静态内部类,简单易懂。

1、饿汉模式

2.单例模式定义

普通的 Java 类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象,这就破坏了单例。而枚举的反序列化并不是通过反射实现的,所以也就不会发生由于反序列化导致的单例破坏问题。

2.1 懒汉、饿汉、变种饿汉(静态代码块实现)模式

这两种模式在实现上都有一定的缺陷,懒汉模式要实现多线程安全就需要对getInstance()方法上加上synchronize 关键字,效率很低,饿汉模式虽然一开始就创建了实例,但是在类加载的时候就创建了对象,不能实现懒加载。

懒汉:

/**
 * Created by jiangcheng on 2017/9/28.
 */
public class Singleton {

    private static Singleton sInstance;
    // 私有构造方法
    private Singleton() {}

    public static synchronized Singleton getInstance () {
        if (sInstance == null) {
            sInstance = new Singleton();
        }
        return sInstance;
    }
}

public class Singleton{

private static Singleton instance = null;

private ingleton(){}

public static Singleton newInstance(){

if(null == instance){

instance =new Singleton();

}

return instance;

}

}

模块:新闻,音乐,视频,图片,唐诗宋词,快递,天气,记事本,阅读器等等

单例模式可以说是最简单的设计模式了,但在使用时也有一些问题需要注意,比如线程安全性和序列化破坏。本文以几个问题为出发点,分析延迟加载、线程安全以及序列化三个方面,深入了解一下单例模式的各种姿势,以便在今后使用时追求极致性能 ⊙﹏⊙‖∣°

2、常见的几种单例模式的实现

2)可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。

5.2 LayoutInflater使用的单例模式

枚举方式实现的单例如下:

2.3 静态内部类(推荐)

public class Singleton {
    private Singleton() {}

    private static class SingletonHolder {
        private static Singleton sInstance = new Singleton();
    }

    public static Singleton getInstance () {
        return SingletonHolder.sInstance;
    }
}

这种方式利用了Java在类加载的时候只允许一个线程加载,它是在内部类里面去创建对象实例。这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。这种方式比较容易看的懂,也很简洁,所以推荐改种实现方式。

这种方式同样利用了类加载机制来保证只创建一个instance实例。它与饿汉模式一样,也是利用了类加载机制,因此不存在多线程并发的问题。不一样的是,它是在内部类里面去创建对象实例。这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。

枚举单例代码

  • 饿汉方式单例到底有多“饿”?
  • 静态内部类为什么是延迟加载的?
  • 枚举方式单例是延迟加载的吗?
  • 饿汉、静态内部类、枚举方式单例为什么是线程安全的?
  • 序列化为什么会破坏单例模式?
  • 怎么防止序列化破坏单例模式?
  • 枚举方式单例是怎么避免序列化破坏的?

2.4 枚举

public enum Singleton {
    instance;
    public void whateverMethod() {
    }
}

由于前面几种都有两个共同的缺点:

  1. 需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。
  2. 可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。

而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,《Effective Java》作者推荐使用的方法。不过,在实际工作中,很少看见有人这么写。

枚举在Java中是很特殊的存在,可以参考Java枚举enum及其应用

2、懒汉模式

枚举单例模式最大的优点就是写法简单,枚举在java中与普通的类是一样的,不仅能够有字段,还能够有自己的方法,最重要的是默认枚举实例是线程安全的,并且在任何情况下,它都是一个单例。即使是在反序列化的过程,枚举单例也不会重新生成新的实例。而其他几种方式,必须加入如下方法:才能保证反序列化时不会生成新的对象。privateObjectreadResolve()throwsObjectStreamException{returnINSTANCE;}

相比于饿汉方式,这种方式实现的单例即使加载了 Singleton 类后,也不一定会创建 Singleton 实例,因为 Singleton 的静态引用放到了静态内部类中,只有静态内部类被加载了,Singleton 实例才会被创建。

从代码中我们看到,类的构造函数定义为private的,保证其他类不能实例化此类,然后提供了一个静态实例并返回给调用者。饿汉模式是最简单的一种实现方式,饿汉模式在类加载的时候就对实例进行创建,实例在整个程序周期都存在。它的好处是只在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题。它的缺点也很明显,即使这个单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了。

5.Android源码中单例

说到饿汉方式往往会提起懒汉方式,对比而言,懒汉方式具有延迟加载(这里的加载指创建 Singleton 实例)的优点。这容易让人对饿汉方式有一个恶劣的刻板印象:它的性能很不好!没有使用它的时候它就会初始化,白白占用资源!

public class Singleton{

private static class SingletonHolder{

public static Singleton instance =new Singleton();

}

private Singleton(){}

public static Singleton newInstance(){

return SingletonHolder.instance;

}

}

静态内部类单例模式public class Singleton { private Singleton (){} ;//私有的构造函数 public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } //定义的静态内部类 private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); //创建实例的地方 } }

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

加锁的懒汉模式看起来即解决了线程并发问题,又实现了延迟加载,然而它存在着性能问题,依然不够完美。synchronized修饰的同步方法比一般方法要慢很多,如果多次调用getInstance(),累积的性能损耗就比较大了。因此就有了双重校验锁,先看下它的实现代码。

publicenumSingleton{  //enum枚举类INSTANCE; public void whateverMethod() { } }

public class Singleton implements Serializable { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton() { } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } //防止序列化破坏单例模式 public Object readResolve() { return SingletonHolder.INSTANCE; }}

public enum Singleton{

总结:不管以哪种形式实现单例模式,它们的核心原理是将构造函数私有化,并且通过静态公有方法获取一个唯一的实例,在这个获取的过程中必须保证线程的安全,同时也要防止反序列化导致重新生成实例对象。

假如我们的单例实现了 serializable 接口,序列化时会通过反射调用无参数的构造方法创建一个新的对象,这时就要重写 readResolve 方法防止序列化破坏单例,如下:

除了上面的三种方式,还有另外一种实现单例的方式,通过静态内部类来实现。首先看一下它的实现代码

通过一个静态方法或者枚举返回单例类对象

我们期望单例模式可以实现只创建一个实例,通过特殊手段创建出其他的实例,就对单例模式造成了破坏。除了反射以外,序列化时也会破坏单例模式。

4、静态内部类

4.单例模式的实现方式

开始正文前先思考下以上问题,如果你都掌握了,就可以点叉出去了。

这种实现方式适合单例占用内存比较小,在初始化时就会被用到的情况。但是,如果单例占用的内存比较大,或单例只是在某个特定场景下才会用到,使用饿汉模式就不合适了,这时候就需要用到懒汉模式进行延迟加载。

确保单例类对象在反序列化时不会重新构造对象

这部分内容其实十分简单。

再来看本文要介绍的最后一种实现方式:枚举。

5.1 InputMethodManager中使用单例模式

本文由必威发布于必威-编程,转载请注明出处:饿汉方式单例到底有多必威:,设计模式之一

TAG标签:
Ctrl+D 将本页面保存为书签,全面了解最新资讯,方便快捷。