高并发下线程安全的单例模式(最全最经典,值得收藏)

作者:mlinge-奋斗吧

blog.csdn.net/cselmu9/article/details/51366946

在所有的设计模式中,单例模式是我们在项目开发中最为常见的设计模式之一,而单例模式有很多种实现方式,你是否都了解呢?高并发下如何保证单例模式的线程安全性呢?如何保证序列化后的单例对象在反序列化后仍然是单例的呢?这些问题在看了本文之后都会一一的告诉你答案,赶快来阅读吧!

什么是单例模式?

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

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

  • 在任何情况下,单例类永远只有一个实例存在
  • 单例需要有能力为整个系统提供这一唯一实例

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

在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。


每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。

正是由于这个特点,单例对象通常作为程序中的存放配置信息的载体,因为它能保证其他对象读到一致的信息。

例如在某个服务器程序中,该服务器的配置信息可能存放在数据库或文件中,这些配置数据由某个单例对象统一读取,服务进程中的其他对象如果要获取这些配置信息,只需访问该单例对象即可。

这种方式极大地简化了在复杂环境 下,尤其是多线程环境下的配置管理,但是随着应用场景的不同,也可能带来一些同步问题。

1、饿汉式单例

饿汉式单例是指在方法调用前,实例就已经创建好了。

高并发下线程安全的单例模式(最全最经典,值得收藏)

以上代码运行结果:

<code>

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

/<code>

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

2、懒汉式单例

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

下面是实现代码:

<code>

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;     } }/<code>

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


高并发下线程安全的单例模式(最全最经典,值得收藏)

执行结果如下:

<code>

1210420568

1210420568

1935123450

1718900954

1481297610

1863264879

369539795

1210420568

1210420568

602269801

/<code>

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

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

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

1.方法中声明synchronized关键字

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


高并发下线程安全的单例模式(最全最经典,值得收藏)

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

<code>

1689058373

1689058373

1689058373

1689058373

1689058373

1689058373

1689058373

1689058373

1689058373

1689058373

/<code>

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

2.同步代码块实现


高并发下线程安全的单例模式(最全最经典,值得收藏)

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

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

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

高并发下线程安全的单例模式(最全最经典,值得收藏)

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

<code>

1481297610

397630378

1863264879

1210420568

1935123450

369539795

590202901

1718900954

1689058373

602269801

/<code>

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

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

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


高并发下线程安全的单例模式(最全最经典,值得收藏)

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

<code>

369539795

369539795

369539795

369539795

369539795

369539795

369539795

369539795

369539795

369539795

/<code>

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

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

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

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

<code>

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;     } }/<code>

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

<code>

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

/<code>

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

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

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

代码实现如下:

<code>

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;     } }/<code>

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

<code>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();         }     } }/<code>

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

<code>

865113938

1442407170

/<code>

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

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

<code>

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;      } }/<code>

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

<code>

865113938

调用了readResolve方法!

865113938

/<code>

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

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

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

<code>

package

 org.mlinge.s08;

public

 

class

 

MySingleton

{     

private

 

static

 MySingleton instance = 

null

;     

private

 

MySingleton

()

{}     

static

{         instance = 

new

 MySingleton();     }     

public

 

static

 MySingleton 

getInstance

()

 

{          

return

 instance;     }  }/<code>

测试代码如下:

<code>

package

 org.mlinge.s08;

public

 

class

 

MyThread

 

extends

 

Thread

{          

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 new MyThread();         }         

for

 (

int

 j = 

0

; j /<code>

运行结果如下:

<code>

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

1718900954

/<code>

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

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

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

<code>

package

 org.mlinge.s09;

public

 

enum

 EnumFactory{      singletonFactory;     

private

 MySingleton instance;     

private

 

EnumFactory

()

{         instance = 

new

 MySingleton();     }     

public

 MySingleton 

getInstance

()

{         

return

 instance;     } }

class

 

MySingleton

{     

public

 

MySingleton

()

{}  }/<code>

测试代码如下:

<code>

package

 org.mlinge.s09;

public

 

class

 

MyThread

 

extends

 

Thread

{          

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 new MyThread();         }         

for

 (

int

 j = 

0

; j /<code>

执行后得到的结果:

<code>

1481297610

1481297610

1481297610

1481297610

1481297610

1481297610

1481297610

1481297610

1481297610

1481297610

/<code>

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

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

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

<code>

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

{     

public

 

MySingleton

()

{}  }/<code>

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

<code>

package

 org.mlinge.s10;

public

 

class

 

MyThread

 

extends

 

Thread

{          

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 new MyThread();         }         

for

 (

int

 j = 

0

; j /<code>

验证结果:

<code>

1935123450

1935123450

1935123450

1935123450

1935123450

1935123450

1935123450

1935123450

1935123450

1935123450

/<code>

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

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

备注:本文的编写思路和实例源码参照《Java多线程编程核心技术》-(高洪岩)一书中第六章的学习案例撰写。


分享到:


相關文章: