二、创建型-单例模式

单例模式是设计模式中使用最为普遍的模式之一,它是一种对象创建模式,用于产生一个对象具体事例,可以确保系统中一个类只产生一个实例。在Java中,这样的行为能带来两大好处:①对于频繁使用的对象可以省略new操作花费的时间,这对于那些重量级对象而言是非常可观的一笔系统开销②由于new操作的次数减少,对系统内存的使用频率也会降低,将减轻GC压力,缩短GC停顿时间。

1、饿汉式
public class SingletonHungry {
    private static SingletonHungry instance= new SingletonHungry();
    private SingletonHungry(){
    }
    public static SingletonHungry getInstance(){
        return instance;
    }
}

该方式性能非常好,getInstance()方法只是简单的返回instance,没有任何锁操作,在并行程序中会有良好的表现。
缺点:SingletonHungry实例在什么时候创建不受控制。对于静态成员instance, 它会在类第一次初始化的时候被创建,但是这个时刻并不一定是getInstance()方法第一次被调用的时候。比方说SingletonHungry中如果还包含一个表示状态的静态成员STATUS:public static int STATUS = 1, 此时在任何地方引用这个STATUS都会导致instance实例被创建(任何对SingletonHungry方法或者字段的引用都会导致类初始化,并创建instance实例,但是类初始化只有一次,因此instance实例永远只会被创建一次),比如外部调用:System.out.println(SingletonHungry.STATUS),此时即使没有要求创建单例,new Singleton()也会被调用。当然,如果这个不足在实际开发中并不重要,那么这种单例模式也是一种不错的选择,它容易实现,代码易读且性能优越。
如果想精确控制instance创建时间,这种方式就不太友善了,需要一种延迟加载策略,只会在instance第一次使用时创建对象。

2、懒汉式--对象创建的时候使用Synchoronized关键字加锁
public class SingletonLazySimple {
    private static SingletonLazySimple instance;
    private SingletonLazySimple(){
    }
    public static synchronized SingletonLazySimple getInstance(){
        if (instance == null) {
            instance = new SingletonLazySimple();
        }
        return instance;
    }
}

这种方式的核心思想是我们不需要实例化instance,当getInstance方法第一次被调用时创建单例对象,为了防止对象被多次创建需要使用synchronized 关键字进行方法同步。好处是充分利用了延迟加载,坏处也很明显:并发环境下加锁竞争激烈的场合对性能产生一定影响。

3、双重校验锁实现
public class SingletonLazyDoubleCheck {
    private volatile static SingletonLazyDoubleCheck instance = null;
    private SingletonLazyDoubleCheck(){

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

和2相比,我们在getInstance()方法上增加了两次非空判断,并且同步代码放在一次instance空判断之后,这样可以一定程度上减少等待,不至于每一个调用该方法的线程都会产生竞争。我们注意到这个时候我们在instance变量上增加了volatile关键字,这是防止指令重排序导致我们在getInstance拿到的对象可能没有完全初始化引发的错误。
在getInstance()方法的instance = new SingletonLazyDoubleCheck()中,虚拟机实际做了三步工作:
1.给SingletonLazyDoubleCheck实例分配内存;
2.调用构造函数初始化成员字段;
3.将instance 对象指向分配的内存空间(instance 不再为null)。
由于指令重排序,执行顺序可能是123、132。多线程情况下假如A线程的3执行完成,2未执行,此时B线程也调用getInstance()方法,此时判断instance 不为空,直接返回instance 对象,此时的对象由于可能在构造函数中有一些初始化操作未完成,拿到该对象去操作可能就会导致使用出现一些问题,此时双重枷锁就会失效,所以需要给singletonLazyDoubleCheck变量加上volatile关键字防止指令重排序。
这种方式设计复杂、丑陋,不推荐使用。

4、内部类实现(最优)
public class SingletonInnerClass {
    private SingletonInnerClass(){
    }
    public static SingletonInnerClass getInstance(){
        return SingletonHolder.instance;
    }
    private static class SingletonHolder{
        private static SingletonInnerClass instance = new SingletonInnerClass();
    }
}

这种方式的优点:
1.getInstance()方法中没有锁,这使得在高并发环境下性能优越
2.只有在getInstance()方法第一次被调用时,SingletonInnerClass的实例才会被创建,因为这种方法使用了内部类和类的初始化方式,内部类SingletonHolder被声明为private,使得不可能在外部访问并初始化它,而我们只能在getInstance()方法内部对SingletonHolder类进行初始化,利用虚拟机的类初始化机制创建单例。

5、枚举实现
public enum  SingletonEnum {
    INSTANCE;

    /**
     * 单利可以有自己的操作
     */
    public void singletonOperation(){
        //功能处理
    }

    public static void main(String[] args) {
        SingletonEnum singletonEnum1 = SingletonEnum.INSTANCE;
        SingletonEnum singletonEnum2 = SingletonEnum.INSTANCE;
        System.out.println(singletonEnum1 == singletonEnum2);
    }
}

优点:①实现简单②枚举本身就是单例模式,由JVM从根本上提供保障,避免通过反射(即使构造函数被私有了也可以通过反射调用)和反序列化的漏洞
缺点:无延迟加载

6、单例模式的问题

反射 反射可以破解上面几种(不包含枚举模式)实现方式:

public class LazySingleton {
    private static LazySingleton instance;
    private LazySingleton() {
    }
    public static synchronized LazySingleton getInstance(){
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}
public static void main(String[] args) throws Exception {
    LazySingleton s1 = LazySingleton.getInstance();
    LazySingleton s2 = LazySingleton.getInstance();
    System.out.println(s1 == s2);
    
    Class<LazySingleton> clazz = (Class<LazySingleton>) Class.forName("com.mobei.lazy.LazySingleton");
    Constructor<LazySingleton> declaredConstructor = clazz.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true);
    LazySingleton lazySingleton1 = declaredConstructor.newInstance();
    LazySingleton lazySingleton2 = declaredConstructor.newInstance();
    System.out.println(lazySingleton1 == lazySingleton2);
}

解决办法:可以在构造方法中手动抛出异常控制

public class LazySingleton {
    private static LazySingleton instance;
    private LazySingleton() {
        if (instance != null) {
            throw new RuntimeException();
        }
    }
    public static synchronized LazySingleton getInstance(){
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

反序列化 反序列化可以破解上面几种(不包含枚举模式)实现方式:

public class LazySingleton implements Serializable {
    private static LazySingleton instance;
    private LazySingleton() {
        if (instance != null) {
            throw new RuntimeException();
        }
    }
    public static synchronized LazySingleton getInstance(){
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}
public static void main(String[] args) throws Exception {
    LazySingleton s1 = LazySingleton.getInstance();

    FileOutputStream fos = new FileOutputStream("d:/a.txt");
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    oos.writeObject(s1);
    oos.close();
    fos.close();

    FileInputStream fis = new FileInputStream("d:/a.txt");
    ObjectInputStream ois = new ObjectInputStream(fis);
    LazySingleton s2 = (LazySingleton) ois.readObject();
    System.out.println(s2 == s1);
}

解决办法:可以通过定义readResolve()防止获得不同对象:反序列化时如果对象所在类定义了readResolve()(实际是一种回调),返回此方法指定的对象

public class LazySingleton implements Serializable {
    private static LazySingleton instance;
    private LazySingleton() {
        if (instance != null) {
            throw new RuntimeException();
        }
    }
    public static synchronized LazySingleton getInstance(){
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }

    //反序列化时,如果定义了readResolve方法则直接返回此方法指定的对象,而不需要再创建新对象
    private Object readResolve() throws ObjectStreamException {
        return instance;
    }
}
7、效率测试
public static void main(String[] args) throws Exception {
    long start = System.currentTimeMillis();
    int threadNum = 10;
    CountDownLatch latch = new CountDownLatch(threadNum);
    for (int i = 0; i < threadNum; i++) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000000; i++) {
                    Object o = LazySingleton.getInstance();
                }
                latch.countDown();
            }
        }).start();
    }
    latch.await();
    long end = System.currentTimeMillis();
    System.out.println("耗时 : " + (end - start));
}
8、常见应用场景

1、项目中读取配置文件的类,一般不需要每次使用配置文件数据都new一个对象去读取
2、日志应用,由于共享的日志文件一直处于打开状态,因此只能有一个实例去操作,否则不好追加
3、数据库连接池的设计
4、Spring中每个Bean默认是单例的,优点是方便Spring容器管理
5、SpringMVC中控制器对象

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容