java设计模式—单例模式

1.单例模式简介

单例模式也被称为单件模式(或单体模式),主要作用是控制某个类的实例的数量是一个,而且只有一个,他关心的是类实例的创建问题,并不关心具体的业务功能。

单例模式的范围:目前Java里面实现的单例是一个ClassLoader及其子类ClassLoader的范围。如果一个虚拟机里面有多个ClassLoader,而且这些ClassLoader都装在某个类的话,就算这个类是单例,也会产生很多个实例。如果一个机器上有多个虚拟机,那么每个虚拟机里面都应该至少产生一个这个类的实例,也就是说整个机器上有很多个实例,也就不是单例了。所以我们这里讨论的单例模式不适应于集群环境。


2.创建单例的步骤

1.构造方法私有化

   想控制实例的只有一个,首先要控制创建实例的地方,构造方法私有化后,类的实例不能由外部创建而只能有自身创建。

2.提供获取实例的方法

  构造方法私有化后,外部类不能创建类实例,此时单例类必须提供向相应的方法来返回类的实例,并且此方法必须是static的,这样才能通过类调用该方法获取实例。

3.定义存储实例的属性

  客户端每次调用创建方法都会创建实例,就成了多个实例;因此要用一个属性来标记自己创建好的实例,第一次创建后保存下来,以后不再创建。该属性会在第一次调用创建实例的静态方法中被创建,所以该属性也就成了一个类变量,有static修饰;

4.实现控制实例的创建

   具体事项发放的实现:根据创建时机不同,分为懒汉式和饿汉式。懒汉式是在第一次调用时创建实例,饿汉式是JVM启动时便已经实例化。


3.单例模式的种类

主要分三种:懒汉式单例、饿汉式单例、登记单是单例。

懒汉式单例

第一次调用时创建单例,不被调用则不会产生实例到内存。

/**
 * Created by duanxiangchao on 2016/7/24.
 * 懒汉式单例
 */
public class LazySingleton {
    /**
     * 私有静态对象,加载时不做初始化
     */
    private static LazySingleton instance = null;

    /**
     * 构造方法私有化,避免外部创建实例
     */
    private LazySingleton(){}

    /**
     * 静态工厂犯法,如果没有实例则初始化,返回唯一实例
     * @return LazySingleton
     */
    private static synchronized LazySingleton getInstance(){
        if(instance == null){
            instance = new LazySingleton();
        }
        return instance;
    }

}

饿汉式单例

JVM启动时创建单例类的实例,如果有过得的单例类,启动时创建太多造成启动变慢,启动时就会占据很多的内存。

/**
 * Created by duanxiangchao on 2016/7/24.
 * 饿汉式单例
 */
public class EagerSingleton {
    /**
     * 私有的唯一实例成员,在类加载的时候创建好了单例对象
     */
    private static final EagerSingleton instance = new EagerSingleton();

    /**
     * 私有构造方法,避免外部创建实例
     */
    private EagerSingleton(){}

    /**
     * 静态工厂方法,返回此类的唯一实例
     * @return  EagerSingleter
     */
    public static EagerSingleton getInstance(){
        return instance;
    }

}

登记式单例

        登记式单例实际上维护的是一组单例类的实例,将这些实例存放在一个Map(登记簿)中,对于已经登记过的实例,则从工厂直接返回,对于没有登记的,则先登记,而后返回。

/**
 * Created by duanxiangchao on 2016/7/24.
 */
public class RegSingleton {
    /**
     * 登记薄,用来存放所有登记的实例
     */
    private static Map<String, RegSingleton> registry = new HashMap<String, RegSingleton>();

    /**
     * 类加载时添加一个实例到登记薄
     */
    static {
        RegSingleton singleton = new RegSingleton();
        registry.put(singleton.getClass().getName(), singleton);
    }
    /**
     * 受保护的默认构造方法
     */
    protected RegSingleton(){}

    /**
     * 静态工厂方法,返回指定登记对象的唯一实例
     * 对于已登记的直接取出返回,对于还未登记的,先登记,后取出返回
     * @param name
     * @return
     */
    public static RegSingleton getInstance(String name){
        if(name == null){
            name = "RegSingleton";
        }
        if(registry.get(name) == null){
            try {
                registry.put(name, (RegSingleton)Class.forName(name).newInstance());
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        return registry.get(name);
    }

}

4.双重检查加锁

       懒汉式的实现是线程安全的,所以会降低整个访问速度,而且每次都要判断一次。有没有更好地实现方式呢?可以使用“双重检查枷锁”机制。
        所谓“双重检查枷锁”机制,并不是每次进入getInstance方法都需要同步,而是先不同步。当进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块后,再检查实例是否存在。如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,创建过程只需要同步一次,从而减少了多次在同步情况下进行判断所浪费的时间。
       在使用“双重检查加锁”机制实现时,需要使用关键字volatile,含义是被volatile修饰的变量的值不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多线程能够正确的处理该变量。JDK1.4及以前的版本,很多JVM对于volatile关键字的实现都存在问题,会导致“双重检查加锁”的失败,因此只能在JDK1.5及以上的版本使用。

示例代码

/**
 * Created by duanxiangchao on 2016/7/26.
 */
public class DoubleCheckSingleton {

    private volatile static DoubleCheckSingleton instance = null;

    private DoubleCheckSingleton(){}

    public static DoubleCheckSingleton getInstance(){
        //先检查实例是否存在,不存在则进入下面的同步块
        if(instance == null){
            //同步块,线程安全的创建实例
            synchronized (DoubleCheckSingleton.class){
                //再次检查是否存在实例,如果不存在,才真正创建实例
                if(instance == null){
                    instance = new DoubleCheckSingleton();
                }
            }
        }
        return instance;
    }

}

双重检查加锁和懒汉式对比

           第一次访问,同时来了多个线程,但重加锁和双重加锁效果一样,此时instance为null,最先获得资源的进行加锁,其余的线程等待,二者效果相同。
       第二次、以后的访问,多个线程访问。对于双重检查加锁,instance不为null,多个线程不用等待,直接返回instance。单重检查,需要等其他线程检查完了才能获取锁进行判断检查,线程越多等待的时间会越长,并发量下降。所以双重检查的第一重检查是很必要的。
为什么还要第二重检查呢?这就是多线程问题
       第一次访问时,多个线程同时访问。假如A线程和B线程同时访问,都判断instance为null,A线程获得对象锁开始创建;B线程等待A线程释放资源,等A线程创建完毕释放资源,这时B线程获得资源,但此时instance实例已经创建,如果不进行二次检查的,就会创建多个实例,就不再是单例,所以第二重检查也必不可少!




5.更好地实现方式

Java的类级内部类方式

       采用静态初始化器的方式,可以由JVM来保证线程安全;采用类级内部类能够让类装载的时候不去初始化对象,在这个类级内部类里面去创建对象实例,这样只要不使用到这个类级内部类,那就不会创建对象实例。同时实现了延迟加载和线程安全。
/**
 * Created by duanxiangchao on 2016/7/26.
 * 类级内部类实例
 */
public class InnerClassSingleton {
    /**
     * 类级别的内部类,也就是静态的成员内部类,该内部类的实例和外部类的实例没有绑定关系,
     * 而且只有被调用到才会装载,从而实现了延迟加载
     */
    private static class SingletonHolder{
        /**
         * 静态初始化器,由JVM来保证线程安全
         */
        private static InnerClassSingleton instance = new InnerClassSingleton();
    }

    /**
     * 私有化构造方法
     */
    private InnerClassSingleton(){}

    public static InnerClassSingleton getInstance(){
        return SingletonHolder.instance;
    }

}
          如上代码,当getInstance()方法第一次被调用时,它第一次读取SingletonHolder.instance,导致SingletonHolder类得到初始化;而这个内部类在装载和初始化的时候,会初始化他的静态域,从而创建InnerClassSingleton的实例,由于是静态的域,因此只会被虚拟机在装在类的时候初始化一次,并有虚拟机来保证他的线程安全。
       这种实现的优势在于,getInstance()方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问开销。

枚举式单例

          单元素的枚举类型已经成为Singleton的最佳方法。Java的枚举类型实质上是功能齐全的类,因此可以有自己的属性和方法。Java枚举类型的基本思想是通过共有的静态final域为每个枚举常量导出实例的类。从某种角度来讲,枚举式单例的泛型化,本质上是单元素的枚举。
        使用枚举实现单例控制,更加简洁,而且武昌地提供了序列化机制,并且由JVM从根本上提供保障,绝对防止多次实例化,是更加简洁、高效、安全的实现单例的方式。
示例代码
/**
 * Created by duanxiangchao on 2016/7/26.
 * 枚举式单例
 */
public enum EnumSingleton {

    /**
     * 单例实例
     */
    INSTANCE;

    /**
     * 示例方法
     */
    public void operate(){
        System.out.println("单例操作......");
    }

}
<pre name="code" class="java">/**
 * Created by duanxiangchao on 2016/7/26.
 */
public class Test {
    public static void main(String[] str){
        //调用枚举单例的方法
        EnumSingleton.INSTANCE.operate();
    }
}

 
 

6.单例的优点、缺点、使用场景。

单例的优点

  (1)  单例模式在内存中只有一个实例,减少了内存消耗,特别是一个对象频繁的创建、销毁,而且创建和销毁时性能又无法优化,这是采用单例模式的优势就非常明显。
 (2) 单例模式只生成一个实例,减少了系统性能开销。当一个对象的产生需要较多资源时,如读取配置文件、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。
 (3) 单例模式可以在系统设置全局的访问点,优化共享资源的访问,例如可以设计一个单例类,负责所有映射表的映射处理。

单例的缺点

  (1) 单例模式没有接口,扩展很困难。如果要扩展,除了修改代码,没有第二种途径可以实现。为什么不能增加接口?因为接口对单例模式没有任何意义,它要求“自行实例化”,并且提供单一实例,接口或抽象类是不能实例化的。
  (2)单例模式对测试时不利的。在并行开发环境中,如果单例模式还没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象。
 (3)单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例类,决定它是不是单例是环境决定的,单例模式把“要单例”和业务逻辑融合在一个类中了。

单例模式的使用场景

    一个系统中,要求一个类有且只有一个对象实例,如果出现多个对象就会出现“不良反应”时,可以采用单例模式。
   (1) 要求生成唯一序列号的环境。
   (2)在整个项目中需要有访问一个共享访问点或共享数据,例如web上的计数器,可以不用每次刷新都计入到数据库中,使用单例模式保持计数器的值,并确保是线程安全的。
    (3)创建一个对象需要消耗的资源过多,如访问I/O、访问数据库等资源。
    (4)需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式。当然也可以直接声明为static的方式。

7.单例模式使用实例   

单例获取properties文件中的配置信息
/**
 * Created by duanxiangchao on 2016/7/26.
 */
public class ConfigSingleton {

    private volatile static ConfigSingleton instance = null;

    private ConfigSingleton(){}

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

    public String get(String key){
        try {
            InputStream inputStream =ClassLoader.getSystemResourceAsStream("config.propeties");
            Properties properties = new Properties();
            properties.load(inputStream);
            return properties.get(key).toString();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

}

只是一个简单的示例,具体大家根据自己的业务要求实现功能!



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值