单例模式

对于单例模式,我想这是大家再熟悉不过的了,也是实际当中用得比较频繁的,对于这个模式可能有点小儿科了,但是,其实可能还有我们未知的一些东东,下面一点一点来挖掘里面的亮点吧!

定义:

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

结构和说明:

对于单例的实现方式,大家应该都知道有两种:懒汉式饿汉式,由于这个都比较熟悉,先贴出两种方式的实现代码:

//懒汉式
public class Singleton {
    // 4:定义一个变量来存储创建好的类实例
    // 5:因为这个变量要在静态方法中使用,所以需要加上static修饰
    private static Singleton instance = null;

    // 1:私有化构造方法,好在内部控制创建实例的数目
    private Singleton() {
    }

    // 2:定义一个方法来为客户端提供类实例
    // 3:这个方法需要定义成类方法,也就是要加static
    public static Singleton getInstance() {
        // 6:判断存储实例的变量是否有值
        if (instance == null) {
            // 6.1:如果没有,就创建一个类实例,并把值赋值给存储类实例的变量
            instance = new Singleton();
        }
        return instance;
    }
}
//饿汉式
public class Singleton {
    // 4:定义一个静态变量来存储创建好的类实例
    // 直接在这里创建类实例,由虚拟机来保证只会创建一次
    private static Singleton instance = new Singleton();

    // 1:私有化构造方法,好在内部控制创建实例的数目
    private Singleton() {
    }

    // 2:定义一个方法来为客户端提供类实例
    // 3:这个方法需要定义成类方法,也就是要加static
    public static Singleton getInstance() {
        // 5:直接使用已经创建好的实例
        return instance;
    }

}

用简单的示意图来进一步描述这两种方式:

分析以上两种方式的实现的特点:

1、懒汉式的实现方式是以时间换空间、并且在多线程环境下是不安全的。

2、饿汉式的实现方式是以空间换时间、并且在多线程环境下是安全的,因为jvm保证了只会装载一次。

对于懒汉式的实现方式而言,为什么会产生多线程环境同步的问题呢?下面对其进行逐依部析,首先先用一个简单的原理图来说明:

说明:同步问题的产生也就是当一个线程还在创建实例并未给实例成员赋值时,而另外一个线程这时发现实现为null,则也进来创建实例,由此会产生了多个实例,而非单例了。

下面用代码来对其多线程安全问题来进行阐述,这样你就可能有个很清楚的理解了:

//懒汉式
public class Singleton {
    private static Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            // 将线程进来时,先休眠2秒钟,这时由于第二个线程先进来,所以它先休眠
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Singleton();
        }
        return instance;
    }

    public static void main(String[] args) {
        // 第一个线程,先休眠700ms,然后再去拿实例
        new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    Thread.sleep(700);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Singleton.getInstance());
            }
        }).start();
        // 第二个线程直接去拿,不等待。
        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println(Singleton.getInstance());
            }
        }).start();
    }
}

看看运行结果:

主要原因是由于线程二在生成实例前休眠了2秒,而线程一在首先休眠7毫秒后,进来判断实例还是为null,这时线程一也进来创建实例了,也就是图中示例所示。

要解决这个线程安全的问题,当然是加同步锁喽,于是乎,给getInstance()加上synchronized,这时再看效果

    public synchronized static Singleton getInstance() {
        if (instance == null) {
            // 将线程进来时,先休眠2秒钟,这时由于第二个线程先进来,所以它先休眠
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Singleton();
        }
        return instance;
    }

哈哈,是不是这种懒汉式的单例模式已经尽乎完美了呢?答案当然不是,来分析下这种在方法上加同步锁带来的问题:

那就是每次在getInstance()时,都会进行锁同步的判断,从性能上来讲,这不是一个很优的方式,那更好性能的线程安全的单例模式有么?答案当然有啦-------双重检查加锁

何为"双重检查加锁"?先解释它的概念,之后会用代码来进行说明:

所谓"双重检查加锁"机制,指的是:并不是每次进入getInstance()都需要同步【这也是上面这种方式的特点】,而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查。进入同步块后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次,从而减少了多次在同步情况下进行判断所浪费的时间了。下面具体来看下代码:

public class Singleton {
    /**
     * 对保存实例的变量添加volatile的修饰
     */
    private volatile static Singleton instance = null;

    private Singleton() {

    }

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

说明:双重检查加锁机制的实现会使用一个关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量

注意:在java1.4及以前版本中,很多JVM对于volatile关键字的实现有问题,会导致双重检查加锁的失败,因此本机制只能用在Java5及以上的版本

对于单例的两种实现方式已经阐述完毕,回过头再来思考思考,实际上这种模式【懒汉式】隐含有两种思想:

1、延迟加载的思想,对于学习java的人,应该都不陌生,这里就不多说,细细体现。

2、缓存的思想,对于什么是缓存,以及怎么在实际当中去使用缓存,应该也是人人皆知的事,对于这种思想,实际上可以启发一种新的单例模式的实现,下面会对其进行说明。

可能对于单例模式的实现,都知道是两种方式去实现,但是,除了这两种方式,还有其它方式也能实现单实例么?这里要介绍一种新的方式,也就是来自于上面所总结的缓存思想

先看代码:

/**
 * 使用缓存来模拟实现单例
 */
public class Singleton {
    /**
     * 定义一个缺省的key值,用来标识在缓存中的存放
     */
    private final static String DEFAULT_KEY = "One";
    /**
     * 缓存实例的容器
     */
    private static Map<String, Singleton> map = new HashMap<String, Singleton>();

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

    public static Singleton getInstance() {
        // 先从缓存中获取
        Singleton instance = (Singleton) map.get(DEFAULT_KEY);
        // 如果没有,就新建一个,然后设置回缓存中
        if (instance == null) {
            instance = new Singleton();
            map.put(DEFAULT_KEY, instance);
        }
        // 如果有就直接使用
        return instance;
    }

    public static void main(String[] args) {

        for (int i = 0; i < 3; i++) {
            System.out.println(Singleton.getInstance());
        }

    }
}

运行结果:

从上面这种缓存实现的单例可以得知:设计模式只是人们经验的总结,实现方式并非是固定不变的。

除了用缓存实现单例,下面要介绍两种更加妙的实现单例的方式,这些方式可能也是大家所不熟知的,所以以后大家不要认为单例就只有两种实现方式哟。

第一种是采用Lazy initialization holder class模式,这个模式综合使用了Java的类级内部类和多线程缺省同步锁的知识,很巧秒的同时实现了延迟加载和线程安全。具体看代码:

public class Singleton {
    /**
     * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例没有绑定关系, 而且只有被调用到才会装载,从而实现了延迟加载
     */
    private static class SingletonHolder {
        /**
         * 静态初始化器,由JVM来保证线程安全
         */
        private static Singleton instance = new Singleton();
    }

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

    public static Singleton getInstance() {
        // 将线程进来时,先休眠2秒钟,这时由于第二个线程先进来,所以它先休眠
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return SingletonHolder.instance;
    }

    public static void main(String[] args) {
        // 第一个线程,先休眠700ms,然后再去拿实例
        new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    Thread.sleep(700);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Singleton.getInstance());
            }
        }).start();
        // 第二个线程直接去拿,不等待。
        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println(Singleton.getInstance());
            }
        }).start();
    }
}

运行结果:

是不是这种单例的实现方式是相当的妙呀,下面还有一种更妙的------采用枚举来实现

该方法的实现来源于《高效java 第二版》中:单元素的枚举类型已经成为实现Singleton的最佳方法

为了理解这个观点,先来了解一下相关的枚举知识,这里只是强化,基本的用法应该是java学习者所熟知的,就不多说了:

1、Java的枚举类型实质上是功能齐全的类,因为可以有自己的属生和方法。

2、Java枚举类型的基本思想:通过公有的静态final域为每个枚举常量导出实例的类。

3、从某个角度讲,枚举是单例的泛型化,本质上是单无素的枚举。

用枚举来实现单例非常简单,只需要编写一个包含单个元素的枚举类型既可,具体代码如下:

Singleton.java:

/**
 * 使用枚举来实现单例模式的示例
 */
public enum Singleton {
    /**
     * 定义一个枚举的元素,它就代表了Singleton的一个实例
     */
    uniqueInstance;

    /**
     * 示意方法,单例可以有自己的操作
     */
    public void singletonOperation() {
        // 打印出hash值,来看是否是同一个实例
        System.out.println("aa==" + Singleton.uniqueInstance.hashCode());
    }
}

Client.java:

public class Client {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            Singleton.uniqueInstance.singletonOperation();
        }
    }
}

运行结果:

上面已经又列出了几种新的实现单例的方法,那回到单例的概念本质来,我们知道它是控制实例数目为1个的,那有没有办法可以控制多于1个的实例呢?答案当然有,请看下面实现:

/**
 * 简单演示如何扩展单例模式,控制实例数目为3个
 */
public class OneExtend {
    /**
     * 定义一个缺省的key值的前缀
     */
    private final static String DEFAULT_PREKEY = "Cache";
    /**
     * 缓存实例的容器
     */

    // 实例调度的问题
    private static Map<String, OneExtend> map = new HashMap<String, OneExtend>();
    /**
     * 用来记录当前正在使用第几个实例,到了控制的最大数目,就返回从1开始
     */
    private static int num = 1;
    /**
     * 定义控制实例的最大数目
     */
    private final static int NUM_MAX = 3;

    private OneExtend() {
    }

    public static OneExtend getInstance() {
        String key = DEFAULT_PREKEY + num;
        OneExtend oneExtend = map.get(key);
        if (oneExtend == null) {
            oneExtend = new OneExtend();
            map.put(key, oneExtend);
        }
        // 把当前实例的序号加1
        num++;
        if (num > NUM_MAX) {
            // 如果实例的序号已经达到最大数目了,那就重复从1开始获取,实际可以有其它调度算法,这里就以简单轮循的方式处理。
            num = 1;
        }
        return oneExtend;
    }

    public static void main(String[] args) {
        OneExtend t1 = getInstance();
        OneExtend t2 = getInstance();
        OneExtend t3 = getInstance();
        OneExtend t4 = getInstance();
        OneExtend t5 = getInstance();
        OneExtend t6 = getInstance();

        System.out.println("t1==" + t1);
        System.out.println("t2==" + t2);
        System.out.println("t3==" + t3);
        System.out.println("t4==" + t4);
        System.out.println("t5==" + t5);
        System.out.println("t6==" + t6);
    }
}

运行结果:

最后在收尾之时,最后说明一下它的使用场景:

当需要控制一个类的实例只能有一个,而且客户只能从一个全局访问点访问它时,可以选用单例模式,这些功能恰好是单例模式要解决的问题。

好啦,单例模式学习到这,我想通过这次的学习,能让我们发现单例隐藏了我们所不知道的东东,好好体会体会,下个模式再见!!

原文地址:https://www.cnblogs.com/webor2006/p/3405074.html