2018-06-13

什么是单例模式?

在文章开始之前我们还是有必要介绍一下什么是单例模式。单例模式是为确保一个类只有一个实例,并为整个系统提供一个全局访问点的一种模式方法。

从概念中体现出了单例的一些特点:

(1)、在任何情况下,单例类永远只有一个实例存在

(2)、单例需要有能力为整个系统提供这一唯一实例 

为了便于读者更好的理解这些概念,下面给出这么一段内容叙述:

在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。

正是由于这个特点,单例对象通常作为程序中的存放配置信息的载体,因为它能保证其他对象读到一致的信息。例如在某个服务器程序中,该服务器的配置信息可能存放在数据库或文件中,这些配置数据由某个单例对象统一读取,服务进程中的其他对象如果要获取这些配置信息,只需访问该单例对象即可。这种方式极大地简化了在复杂环境 下,尤其是多线程环境下的配置管理,但是随着应用场景的不同,也可能带来一些同步问题。


各式各样的单例实现

温馨提示:本文叙述中涉及到的相关源码可以在这里进行下载源码,读者可免积分下载。

1、饿汉式单例

饿汉式单例是指在方法调用前,实例就已经创建好了。下面是实现代码:

package org.mlinge.s01;

public class MySingleton {

private static MySingleton instance = new MySingleton();

private MySingleton(){}

public static MySingleton getInstance() {

return instance;

}

}

以上是单例的饿汉式实现,我们来看看饿汉式在多线程下的执行情况,给出一段多线程的执行代码:

package org.mlinge.s01;

public class MyThread extends Thread{


@Override

public void run() {

System.out.println(MySingleton.getInstance().hashCode());

}

public static void main(String[] args) {

MyThread[] mts = new MyThread[10];

for(int i = 0 ; i < mts.length ; i++){

mts[i] = new MyThread();

}

for (int j = 0; j < mts.length; j++) {

mts[j].start();

}

}

}

以上代码运行结果:

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

从运行结果可以看出实例变量额hashCode值一致,这说明对象是同一个,饿汉式单例实现了。

2、懒汉式单例

懒汉式单例是指在方法调用获取实例时才创建实例,因为相对饿汉式显得“不急迫”,所以被叫做“懒汉模式”。下面是实现代码:

package org.mlinge.s02;

public class MySingleton {

private static MySingleton instance = null;

private MySingleton(){}

public static MySingleton getInstance() {

if(instance == null){//懒汉式

instance = new MySingleton();

}

return instance;

}

}

这里实现了懒汉式的单例,但是熟悉多线程并发编程的朋友应该可以看出,在多线程并发下这样的实现是无法保证实例实例唯一的,甚至可以说这样的失效是完全错误的,下面我们就来看一下多线程并发下的执行情况,这里为了看到效果,我们对上面的代码做一小点修改:

package org.mlinge.s02;

public class MySingleton {

private static MySingleton instance = null;

private MySingleton(){}

public static MySingleton getInstance() {

try {

if(instance != null){//懒汉式

}else{

//创建实例之前可能会有一些准备性的耗时工作

Thread.sleep(300);

instance = new MySingleton();

}

} catch (InterruptedException e) {

e.printStackTrace();

}

return instance;

}

}

这里假设在创建实例前有一些准备性的耗时工作要处理,多线程调用:

package org.mlinge.s02;

public class MyThread extends Thread{


@Override

public void run() {

System.out.println(MySingleton.getInstance().hashCode());

}

public static void main(String[] args) {

MyThread[] mts = new MyThread[10];

for(int i = 0 ; i < mts.length ; i++){

mts[i] = new MyThread();

}

for (int j = 0; j < mts.length; j++) {

mts[j].start();

}

}

}

执行结果如下:

1210420568

1210420568

1935123450

1718900954

1481297610

1863264879

369539795

1210420568

1210420568

602269801

从这里执行结果可以看出,单例的线程安全性并没有得到保证,那要怎么解决呢?

3、线程安全的懒汉式单例

要保证线程安全,我们就得需要使用同步锁机制,下面就来看看我们如何一步步的解决 存在线程安全问题的懒汉式单例(错误的单例)。

(1)、 方法中声明synchronized关键字

出现非线程安全问题,是由于多个线程可以同时进入getInstance()方法,那么只需要对该方法进行synchronized的锁同步即可:

package org.mlinge.s03;

public class MySingleton {

private static MySingleton instance = null;

private MySingleton(){}

public synchronized static MySingleton getInstance() {

try {

if(instance != null){//懒汉式

}else{

//创建实例之前可能会有一些准备性的耗时工作

Thread.sleep(300);

instance = new MySingleton();

}

} catch (InterruptedException e) {

e.printStackTrace();

}

return instance;

}

}

此时任然使用前面验证多线程下执行情况的MyThread类来进行验证,将其放入到org.mlinge.s03包下运行,执行结果如下:

1689058373

1689058373

1689058373

1689058373

1689058373

1689058373

1689058373

1689058373

1689058373

1689058373

从执行结果上来看,问题已经解决了,但是这种实现方式的运行效率会很低。同步方法效率低,那我们考虑使用同步代码块来实现:

(2)、 同步代码块实现

package org.mlinge.s03;

public class MySingleton {

private static MySingleton instance = null;

private MySingleton(){}

//public synchronized static MySingleton getInstance() {

public static MySingleton getInstance() {

try {

synchronized (MySingleton.class) {

if(instance != null){//懒汉式

}else{

//创建实例之前可能会有一些准备性的耗时工作

Thread.sleep(300);

instance = new MySingleton();

}

}

} catch (InterruptedException e) {

e.printStackTrace();

}

return instance;

}

}

这里的实现能够保证多线程并发下的线程安全性,但是这样的实现将全部的代码都被锁上了,同样的效率很低下。

(3)、 针对某些重要的代码来进行单独的同步(可能非线程安全)

针对某些重要的代码进行单独的同步,而不是全部进行同步,可以极大的提高执行效率,我们来看一下:

package org.mlinge.s04;

public class MySingleton {

private static MySingleton instance = null;

private MySingleton(){}

public static MySingleton getInstance() {

try { 

if(instance != null){//懒汉式

}else{

//创建实例之前可能会有一些准备性的耗时工作

Thread.sleep(300);

synchronized (MySingleton.class) {

instance = new MySingleton();

}

}

} catch (InterruptedException e) {

e.printStackTrace();

}

return instance;

}

}

此时同样使用前面验证多线程下执行情况的MyThread类来进行验证,将其放入到org.mlinge.s04包下运行,执行结果如下:

1481297610

397630378

1863264879

1210420568

1935123450

369539795

590202901

1718900954

1689058373

602269801

从运行结果来看,这样的方法进行代码块同步,代码的运行效率是能够得到提升,但是却没能保住线程的安全性。看来还得进一步考虑如何解决此问题。

(4)、 Double Check Locking 双检查锁机制(推荐)

为了达到线程安全,又能提高代码执行效率,我们这里可以采用DCL的双检查锁机制来完成,代码实现如下:

package org.mlinge.s05;

public class MySingleton {

//使用volatile关键字保其可见性

volatile private static MySingleton instance = null;

private MySingleton(){}

public static MySingleton getInstance() {

try { 

if(instance != null){//懒汉式

}else{

//创建实例之前可能会有一些准备性的耗时工作

Thread.sleep(300);

synchronized (MySingleton.class) {

if(instance == null){//二次检查

instance = new MySingleton();

}

}

}

} catch (InterruptedException e) {

e.printStackTrace();

}

return instance;

}

}

将前面验证多线程下执行情况的MyThread类放入到org.mlinge.s05包下运行,执行结果如下:

369539795

369539795

369539795

369539795

369539795

369539795

369539795

369539795

369539795

369539795

从运行结果来看,该中方法保证了多线程并发下的线程安全性。

这里在声明变量时使用了volatile关键字来保证其线程间的可见性;在同步代码块中使用二次检查,以保证其不被重复实例化。集合其二者,这种实现方式既保证了其高效性,也保证了其线程安全性。

4、使用静态内置类实现单例模式

DCL解决了多线程并发下的线程安全问题,其实使用其他方式也可以达到同样的效果,代码实现如下:

package org.mlinge.s06;

public class MySingleton {

//内部类

private static class MySingletonHandler{

private static MySingleton instance = new MySingleton();

}

private MySingleton(){}

public static MySingleton getInstance() {

return MySingletonHandler.instance;

}

}

以上代码就是使用静态内置类实现了单例模式,这里将前面验证多线程下执行情况的MyThread类放入到org.mlinge.s06包下运行,执行结果如下:

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

从运行结果来看,静态内部类实现的单例在多线程并发下单个实例得到了保证。

5、序列化与反序列化的单例模式实现

静态内部类虽然保证了单例在多线程并发下的线程安全性,但是在遇到序列化对象时,默认的方式运行得到的结果就是多例的。

代码实现如下:

package org.mlinge.s07;

import java.io.Serializable;

public class MySingleton implements Serializable {

private static final long serialVersionUID = 1L;

//内部类

private static class MySingletonHandler{

private static MySingleton instance = new MySingleton();

}

private MySingleton(){}

public static MySingleton getInstance() {

return MySingletonHandler.instance;

}

}

序列化与反序列化测试代码:

package org.mlinge.s07;

import java.io.File;

import java.io.FileInputStream;

import java.io.FileNotFoundException;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.ObjectInputStream;

import java.io.ObjectOutputStream;

public class SaveAndReadForSingleton {

public static void main(String[] args) {

MySingleton singleton = MySingleton.getInstance();

File file = new File("MySingleton.txt");

try {

FileOutputStream fos = new FileOutputStream(file);

ObjectOutputStream oos = new ObjectOutputStream(fos);

oos.writeObject(singleton);

fos.close();

oos.close();

System.out.println(singleton.hashCode());

} catch (FileNotFoundException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}

try {

FileInputStream fis = new FileInputStream(file);

ObjectInputStream ois = new ObjectInputStream(fis);

MySingleton rSingleton = (MySingleton) ois.readObject();

fis.close();

ois.close();

System.out.println(rSingleton.hashCode());

} catch (FileNotFoundException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

} catch (ClassNotFoundException e) {

e.printStackTrace();

}

}

}

运行以上代码,得到的结果如下:

865113938

1442407170

从结果中我们发现,序列号对象的hashCode和反序列化后得到的对象的hashCode值不一样,说明反序列化后返回的对象是重新实例化的,单例被破坏了。那怎么来解决这一问题呢?

解决办法就是在反序列化的过程中使用readResolve()方法,单例实现的代码如下:

package org.mlinge.s07;

import java.io.ObjectStreamException;

import java.io.Serializable;

public class MySingleton implements Serializable {

private static final long serialVersionUID = 1L;

//内部类

private static class MySingletonHandler{

private static MySingleton instance = new MySingleton();

}

private MySingleton(){}

public static MySingleton getInstance() {

return MySingletonHandler.instance;

}

//该方法在反序列化时会被调用,该方法不是接口定义的方法,有点儿约定俗成的感觉

protected Object readResolve() throws ObjectStreamException {

System.out.println("调用了readResolve方法!");

return MySingletonHandler.instance;

}

}

再次运行上面的测试代码,得到的结果如下:

865113938

调用了readResolve方法!

865113938

从运行结果可知,添加readResolve方法后反序列化后得到的实例和序列化前的是同一个实例,单个实例得到了保证。

6、使用static代码块实现单例

静态代码块中的代码在使用类的时候就已经执行了,所以可以应用静态代码块的这个特性的实现单例设计模式。

package org.mlinge.s08;

public class MySingleton{

private static MySingleton instance = null;

private MySingleton(){}

static{

instance = new MySingleton();

}

public static MySingleton getInstance() {

return instance;

}

}

测试代码如下:

package org.mlinge.s08;

public class MyThread extends Thread{


@Override

public void run() {

for (int i = 0; i < 5; i++) {

System.out.println(MySingleton.getInstance().hashCode());

}

}

public static void main(String[] args) {

MyThread[] mts = new MyThread[3];

for(int i = 0 ; i < mts.length ; i++){

mts[i] = new MyThread();

}

for (int j = 0; j < mts.length; j++) {

mts[j].start();

}

}

}

运行结果如下:

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

从运行结果看,单例的线程安全性得到了保证。

7、使用枚举数据类型实现单例模式

枚举enum和静态代码块的特性相似,在使用枚举时,构造方法会被自动调用,利用这一特性也可以实现单例:

package org.mlinge.s09;

public enum EnumFactory{


    singletonFactory;


    private MySingleton instance;


    private EnumFactory(){//枚举类的构造方法在类加载是被实例化

        instance = new MySingleton();

    }


    public MySingleton getInstance(){

        return instance;

    }


}

class MySingleton{//需要获实现单例的类,比如数据库连接Connection

    public MySingleton(){}

}

测试代码如下:

package org.mlinge.s09;

public class MyThread extends Thread{


@Override

public void run() {

System.out.println(EnumFactory.singletonFactory.getInstance().hashCode());

}

public static void main(String[] args) {

MyThread[] mts = new MyThread[10];

for(int i = 0 ; i < mts.length ; i++){

mts[i] = new MyThread();

}

for (int j = 0; j < mts.length; j++) {

mts[j].start();

}

}

}

执行后得到的结果:

1481297610

1481297610

1481297610

1481297610

1481297610

1481297610

1481297610

1481297610

1481297610

1481297610

运行结果表明单例得到了保证,但是这样写枚举类被完全暴露了,据说违反了“职责单一原则”,那我们来看看怎么进行改造呢。

8、完善使用enum枚举实现单例模式

不暴露枚举类实现细节的封装代码如下:

package org.mlinge.s10;

public class ClassFactory{

private enum MyEnumSingleton{

singletonFactory;

private MySingleton instance;

private MyEnumSingleton(){//枚举类的构造方法在类加载是被实例化

instance = new MySingleton();

}

public MySingleton getInstance(){

return instance;

}

}

public static MySingleton getInstance(){

return MyEnumSingleton.singletonFactory.getInstance();

}

}

class MySingleton{//需要获实现单例的类,比如数据库连接Connection

public MySingleton(){}

}

验证单例实现的代码如下:

package org.mlinge.s10;

public class MyThread extends Thread{


@Override

public void run() {

System.out.println(ClassFactory.getInstance().hashCode());

}

public static void main(String[] args) {

MyThread[] mts = new MyThread[10];

for(int i = 0 ; i < mts.length ; i++){

mts[i] = new MyThread();

}

for (int j = 0; j < mts.length; j++) {

mts[j].start();

}

}

}

验证结果:

1935123450

1935123450

1935123450

1935123450

1935123450

1935123450

1935123450

1935123450

1935123450

1935123450

验证结果表明,完善后的单例实现更为合理。

以上就是本文要介绍的所有单例模式的实现,相信认真阅读的读者都已经明白文章开头所引入的那几个问题了,祝大家读得开心:-D!

---------------------

作者:mlinge-奋斗吧

来源:CSDN

原文:https://blog.csdn.net/cselmu9/article/details/51366946

版权声明:本文为博主原创文章,转载请附上博文链接!

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

推荐阅读更多精彩内容

  • 一、wait--notify--sleep Object obj = new Object(); obj.wait...
    fe0180bd6eaf阅读 331评论 0 1
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,604评论 18 399
  • 犹惧佳节至, 不见旧人旁。 静时思音貌, 独坐忆热肠。 觥筹影交错, 枯笑掩心霜。 何须夜入梦, 颊上泪两行。
    海曲三少阅读 229评论 6 4
  • 刚刚到手的新车上充满了胶水味、皮革味、塑料味、烟味、汗味让车主很头痛,好像怎么除都除不尽,开着真的很不舒服。没关系...
    黄江水车佬阿坚阅读 373评论 0 0
  • 以文辅政,做不到这一点,再漂亮的文字也只能成为一堆废纸。对这一点,或许无需多说,稍有常识的都懂得。问题在于...
    一行西去阅读 170评论 2 1