JAVA设计模式(1) - 单例模式


该文章首发于:个人博客-JAVA设计模式,欢迎关注


介绍

定义

​ 某个类对象只有一个实例,并提供一个公共构造方法,使其项目内有且只能访问这个唯一的对象。

结构

单例模式,其中单例类为Server,访问类为Client

特点

  • 一个类只能有一个实例

  • 类本身有公共构造方法,提供全局使用

  • 不能作为抽象类

应用

​ 项目中,遇到以下情况可以考虑用单例模式:

  • 项目只需要一个实例对象: 全局都要用到的工具类,管理类,服务类,具体如"任务管理服务","打印管理服务","环境监测工具"等。

  • 项目中一个类需要共享: 共享对象可以节约内存,并加快访问速度,如项目中的"全局配置类","数据库连接池"等。

  • 某个类需要频繁创建和销毁对象: 运用单例类可以减轻内存抖动,把操作控制在最小范围内,如"网络连接池","考勤管理类"等。

懒汉模式

​ 该类加载时没有生成单例,只有当第一次调用 getInstance() 时才会去创建这个单例,如下:


public class LazySingleton {



    private static LazySingleton instance = null;

    private LazySingleton(){};



    public static LazySingleton getInstance() {

        if (instance == null) instance = new LazySingleton();

        return instance;

    }

}

​ 上面这段代码不能保证线程安全,因此可以加上 synchronizedvolatile 关键字来保证安全,但每次访问该方法都会上锁同步,影响了性能且消耗更多资源,这也是懒汉模式的缺点。

饿汉模式

​ 该类加载时就同时创建好了静态对象,以后不再改变,且是线程安全的,如下:


public class HungrySingleton {



    private static final HungrySingleton instance = new HungrySingleton();

    private HungrySingleton(){};



    public static  HungrySingleton getInstance() {

        return instance;

    }

}

​ 上面这段代码表示,类初始化的时候就已经同步创建好了静态对象供系统调用。

总结

优点

  • 一个类只有一个实例,避免了内存的多余开销,也方便项目代码的管理

  • 避免资源的多重占用问题

缺点

  • 职责过重,类内部管理逻辑复杂

  • 没有抽象层,限制了扩展性

优化

​ 上文提到的懒汉模式是比较常用的单例类型,但存在线程安全的问题,多线程的情况下可能出现多个线程读取到的instance都为null,就会创建多个对象,从而导致单例模式失效。

​ 因此,我们可以锁机制,给实例方法加锁:


public class LazySingleton {



    private static LazySingleton instance = null;

    private LazySingleton(){};

    // 给方法本身加锁

    public static synchronized LazySingleton getInstance() {

        if (instance == null) instance = new LazySingleton();

        return instance;

    }

}

​ 给方法本身上锁,就很好地避免了多线程同步带来的困扰,但也同时损耗了很多性能,因为给整个方法上锁,无论是否要同步都得先经过锁机制。

​ 同步抢占一个对象的情况很少发生,所以我们引入了双重校验锁机制:


public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton(){};

// 双重校验锁

    public static LazySingleton getInstance() {

        if (instance == null) {

            synchronized (LazySingleton.class) {

                if (instance == null) instance = new LazySingleton();

            }

        }

        return instance;

    }

}

​ 以上方法通过同步代码块而不是同步方法的方案来改进性能,只有在未初始化时才会加锁创建对象,否则就直接返回对象。

​ 但该方法还有弊端,在某些编译器上,两个线程同时引用getInsntance()方法,其中一个线程依然有可能获取到尚未构造完成的对象。如代码中


instance = new LazySingleton();

编译器翻译成字节码,里面包含了四个步骤:

A:申请内存空间

B:赋予默认值

C:执行构造器方法

D:连接引用和实例

其中执行的步骤顺序有可能是ABCD,也有可能是ABDC,那如果是后者,就会导致另一个线程获取到的对象是未构造完成的。因此我们继续优化,给instance加上volatile关键字,禁止重排序,确保编译器是执行的步骤是ABCD,也就完善了我们的双重校验锁机制:


public class LazySingleton {

// 加上了 volatile 关键字

    private static volatile LazySingleton instance = null;

    private LazySingleton(){};

// 双重校验锁

    public static LazySingleton getInstance() {

        if (instance == null) {

            synchronized (LazySingleton.class) {

                if (instance == null) instance = new LazySingleton();

            }

        }

        return instance;

    }

}

​ 以上代码很好地解决了多个线程同步导致的对象抢占问题,但还有一个弊端,那就是在对象可被序列化时,在序列化的途中,编译器会通过反射的方式来在内部调用一个构造方法,从而生成新对象,破坏单例结构。

​ 因此,我们可以继续优化,比如在类内部添加一个readReslove(),但在JDK-1.5以后,我们可以通过定义枚举的方式,同时解决"线程安全"和"序列化破坏单例"的问题,如下:


public enum Singleton {

    INSTANCE;

    public void xxx() {};

}

​ 为什么以上代码就能同时解决”线程安全“和”序列化破坏单例“的问题呢,因为编译器在编译枚举类型时,各个枚举项都编译为static final类型,即这个枚举项在类加载的时候就被初始化了,而我们知道,JAVA类的加载和初始化就是线程安全的,底层已经包我们做好了这套机制,因此可以认为枚举是线程安全的

​ 那为什么枚举类型又可以避免序列化导致的单例破坏呢?我们可以在oracle文档里找到答案:

1.12 Serialization of Enum Constants

Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form. To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant's name method. To deserialize an enum constant, ObjectInputStream reads the constant name from the stream; the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method, passing the constant's enum type along with the received constant name as arguments. Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream.

The process by which enum constants are serialized cannot be customized: any class-specific writeObject, readObject, readObjectNoData, writeReplace, and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or serialVersionUID field declarations are also ignored--all enum types have a fixed serialVersionUID of 0L. Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent.

大概意思就是:枚举不同于常规的类型,序列化的时候仅输出枚举的名字,并没有真正的局部值,而同时反序列化的时候,也是通过内部定义的valueof()方法来通过名字找到枚举对象。同时常规的序列化方法如writeObject(),readObject(),readResove()等 也都被禁用。即枚举内部的序列化与反序列化独立于常规机制,内部做了规范,因此可以认为枚举类是可以解决反射破坏单例问题的

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

推荐阅读更多精彩内容