单例模式(创建型)

  单例模式的主要作用是保证在java程序中,某个类只有一个实例存在,许多时候整个系统只需要拥有一个的全局对象,这样有利于协调系统整体的行为。
  一些管理器和控制器常被设计成单例模式,例如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象在通过这个单例对象获取这些配置信息;再比如android中的关于SystemSetting中的各种Manager和Service。
  这种方式简化了在复杂环境下的系统配置和管理。如果一个对象有可能贯穿整个应用程序,而且起到了全局统一管理控制的作用,那么单例模式是一个很好的选择。
单例模式有很多写法,下面分别进行介绍。
1. 饿汉模式

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

  从代码中可以了解到,构造方法设为private,这样保证了其他类无法实例化此类,然后提供一个静态的方法经静态的实力返回给调用者。这种模式在类加载的时候就对实例进行创建了,并且实例在整个程序周期都存在。它的好处就是只在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题。缺点就是不管这个类有没有用到都会被创建实例化,就可能出现内存浪费的情况,并且会增加程序启动的时间。
  这个实现方式适用于单例占用内存较小,初始化时就会被用到的情况下。但是,如果单例占用的内存较大,或者是只有在某特定的情况下才用到(例如在测试环境下才会使用),这个方式就不合适了。这是就需要用懒汉模式进行延迟加载。
2. 懒汉模式

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

  这种方式是在需要的时候才去创建的,如果单例已经创建好了,再次去调用获取接口将不会重新创建新的对象。如果某个单例使用次数少,并且创建单例消耗的资源较多,那么就需要实现单例的按需创建,这是懒汉模式就派上用场了。但是这样写有一个隐患,就是没有考虑到多线程安全的问题,因此需要加锁,如下:

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

  加锁后看起来解决了多线程并发的问题,有实现了延迟加载,但还是存在性能问题,因为synchronized修饰的同步方法比一般方法要慢很多,多次调用getInstance(),累计的性能消耗就比较可观了。那么我们来看看下面改进的双重校验锁的实现。
3. 双重校验锁

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

  从代码看,大部分情况下,调用getInstance()都不会执行到同步代码块,从而提高了程序性能。在同步代码块中加入第二次判空的语句就是为了防止少数的情况的发生:假如两个线程A、B,A执行了if (instance == null)语句,它会认为单例对象没有创建,此时线程切到B也执行了同样的语句,B也认为单例对象没有创建,然后两个线程依次执行同步代码块,并分别创建了一个单例对象。
  双重校验锁现在看起来解决了线程并发的问题,但还不是万无一失。
这里就要提到java中的指令重排优化,即在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。JVM中没用规定编译器优化相关的内容,自然JVM就可以自由的进行指令重拍的优化,也可称为指令乱序。
  这个问题的关键在于指令重拍优化的存在,导致初始化Singleton和将对象地址赋给instance字段的顺序是不确定的。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,但是该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance(),取到的就是状态不正确的对象,程序便会出错。
  上述便是双重校验锁会失效的原因,不过JDK1.5之后版本增加了volatile关键字,该关键字的一个语义就是禁止指令重排序优化,这样就会避免上述问题。代码如下:

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

  
4. 静态内部类(推荐使用)

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

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

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

  上面提到的四种单例实现的方式都有共同缺点:
  1.需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。
  2.可以使用反射强行调用私有构造器(如果要便面这种情况,可以修改构造器,让他在创建第二个实例时抛异常)
  枚举很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自己序列化机制,防止反序列化的时候创建新的对象。


参考文章:http://blog.csdn.net/goodlixueyong/article/details/51935526
其实基本上都是照抄的,动手敲一遍理解更深刻

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值