转载

Java 多线程核心技术 (五):单例模式与多线程

本文经授权转载自微信公众号薛勤的博客,本文只需要考虑一件事:如何使单例模式遇到多线程是安全的、正确的。

1. 立即加载 / “饿汉模式”

什么是立即加载?立即加载就是使用类的时候已经将对象创建完毕,常见的实现办法就是直接 new 实例化。

复制代码

publicclassMyObject{
privatestaticMyObject myObject =newMyObject();

publicMyObject(){

}

publicstaticMyObjectgetInstance(){
returnmyObject;
}


publicstaticvoidmain(String[] args){
newThread(newRunnable() {
@Override
publicvoidrun() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
newThread(newRunnable() {
@Override
publicvoidrun() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
newThread(newRunnable() {
@Override
publicvoidrun() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
}

}

打印结果:

复制代码

控制台打印的 hashCode 是同一个值,说明对象是同一个,也就实现了立即加载型单例设计模式。

此版本的缺点是不能有其他其他实例变量,因为 getInstance() 方法没有同步,所以有可能出现非线程安全问题。

2. 延迟加载 / “懒汉模式”

什么是延迟加载?延迟加载就是在调用 get() 方法时实例才被创建,常见的实现方法就是在 get() 方法中进行 new() 实例化。

测试代码:

复制代码

publicclassMyObject{
privatestaticMyObject myObject;

publicMyObject(){

}

publicstaticMyObjectgetInstance(){
try{
if(myObject ==null) {
// 模拟对象在创建之前做的一些准备工作
Thread.sleep(3000);
myObject =newMyObject();
}
}catch(InterruptedException e) {
e.printStackTrace();
}
returnmyObject;
}


publicstaticvoidmain(String[] args){
newThread(newRunnable() {
@Override
publicvoidrun() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
newThread(newRunnable() {
@Override
publicvoidrun() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
newThread(newRunnable() {
@Override
publicvoidrun() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
}

}

打印结果:

复制代码

从运行结果来看,创建了三个对象,并不是真正的单例模式。原因显而易见,3 个线程同时进入了 if (myObject == null) 判断语句中,最后各自都创建了对象。

3. 延迟加载解决方案

3.1 声明 synchronized 关键字

既然多个线程可以同时进入 getInstance() 方法,那么只需要对其进行同步 synchronized 处理即可。

复制代码

publicclassMyObject{
privatestaticMyObject myObject;

publicMyObject(){

}

synchronizedpublicstaticMyObjectgetInstance(){
try{
if(myObject ==null) {
// 模拟对象在创建之前做的一些准备工作
Thread.sleep(3000);
myObject =newMyObject();
}
}catch(InterruptedException e) {
e.printStackTrace();
}
returnmyObject;
}


publicstaticvoidmain(String[] args){
newThread(newRunnable() {
@Override
publicvoidrun(){
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
newThread(newRunnable() {
@Override
publicvoidrun(){
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
newThread(newRunnable() {
@Override
publicvoidrun(){
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
}

}

打印结果:

复制代码

虽然运行结果表明,成功实现了单例,但这种给整个方法上锁的解决方法效率太低。

3.2 尝试同步 synchronized 代码块

同步方法是对方法的整体加锁,这对运行效率来讲很不利的。改成同步代码块后:

复制代码

publicclassMyObject {
privatestatic MyObject myObject;

publicMyObject(){

}

public static MyObject getInstance(){
try{
synchronized (MyObject.class) {
if(myObject==null) {
// 模拟对象在创建之前做的一些准备工作
Thread.sleep(3000);
myObject =newMyObject();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
}


public static void main(String[]args) {
newThread(newRunnable(){
@Override
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
newThread(newRunnable(){
@Override
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
newThread(newRunnable(){
@Override
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
}

}

打印结果:

复制代码

运行结果虽然表明是正确的,但同步 synchronized 语句块依旧把整个 getInstance() 方法代码包括在内,和 synchronize 同步方法效率是一样低下。

3.3 针对某些重要的代码进行单独同步

所以,我们可以针对某些重要的代码进行单独的同步,而其他的代码则不需要同步。这样在运行时,效率完全可以得到大幅度提升。

复制代码

publicclassMyObject {
privatestatic MyObject myObject;

publicMyObject(){

}

public static MyObject getInstance(){
try{
if(myObject==null) {
// 模拟对象在创建之前做的一些准备工作
Thread.sleep(3000);
synchronized (MyObject.class) {
myObject =newMyObject();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
}


public static void main(String[]args) {
newThread(newRunnable(){
@Override
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
newThread(newRunnable(){
@Override
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
newThread(newRunnable(){
@Override
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
}

}

运行结果:

复制代码

此方法只对实例化对象的关键代码进行同步,从语句的结构上来说,运行的效率的确得到的提升。但是在多线程的情况下依旧无法解决得到一个单例对象的结果。

3.4 使用 DCL 双检查锁机制

在最后的步骤中,使用 DCL 双检查锁机制来实现多线程环境中的延迟加载单例设计模式。

复制代码

publicclassMyObject{
privatevolatilestaticMyObject myObject;

publicMyObject(){

}

publicstaticMyObjectgetInstance(){
try{
if(myObject ==null) {
// 模拟对象在创建之前做的一些准备工作
Thread.sleep(3000);
synchronized(MyObject.class) {
if(myObject ==null) {
myObject =newMyObject();
}
}
}
}catch(InterruptedException e) {
e.printStackTrace();
}
returnmyObject;
}


publicstaticvoidmain(String[] args){
newThread(newRunnable() {
@Override
publicvoidrun(){
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
newThread(newRunnable() {
@Override
publicvoidrun(){
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
newThread(newRunnable() {
@Override
publicvoidrun(){
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
}

}

运行结果:

复制代码

使用 DCL 双重检查锁功能,成功地解决了“懒汉模式”遇到多线程的问题。DCL 也是大多数多线程结合单例模式使用的解决方案。

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

DCL 可以解决多线程单例模式的非线程安全问题。当然,还有许多其它的方法也能达到同样的效果。

复制代码

publicclassMyObject{
publicstaticclassMyObjectHandle{
privatestaticMyObject myObject =newMyObject();
publicstaticMyObjectgetInstance(){
returnmyObject;
}
}

publicstaticMyObjectgetInstance(){
returnMyObjectHandle.getInstance();
}

publicstaticvoidmain(String[] args){
newThread(newRunnable() {
@Override
publicvoidrun() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
newThread(newRunnable() {
@Override
publicvoidrun() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
newThread(newRunnable() {
@Override
publicvoidrun() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
}
}

打印结果:

复制代码

静态内置类可以达到线程安全问题,但如果遇到序列化对象时,使用默认的方式运行得到的结果还是多例的。

解决方法就是在反序列化中使用 readResolve() 方法:

复制代码

publicclassMyObject implements Serializable {
// 静态内部类
public staticclassMyObjectHandle{
privatestatic final MyObject myObject =newMyObject();
}

public static MyObject getInstance(){
returnMyObjectHandle.myObject;
}

protected Object readResolve(){
System.out.println(" 调用了 readResolve 方法 ");
returnMyObjectHandle.myObject;
}

public static void main(String[]args) throws IOException, ClassNotFoundException {
MyObject myObject =MyObject.getInstance();
FileOutputStream outputStream =newFileOutputStream(newFile("myObject.txt"));
ObjectOutputStream objectOutputStream =newObjectOutputStream(outputStream);
objectOutputStream.writeObject(myObject);
objectOutputStream.close();
System.out.println(myObject.hashCode());

FileInputStream inputStream =newFileInputStream(newFile("myObject.txt"));
ObjectInputStream objectInputStream =newObjectInputStream(inputStream);
MyObject object = (MyObject) objectInputStream.readObject();
objectInputStream.close();
System.out.println(object.hashCode());
}
}

运行结果:

复制代码

621009875
调用了 readResolve 方法
621009875

5. 使用 static 代码块实现单例模式

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

复制代码

publicclassMyObject{
privatestaticMyObject myObject =null;
static{
myObject =newMyObject();
}
publicstaticMyObjectgetInstance(){
returnmyObject;
}
publicstaticvoidmain(String[] args){
newThread(newRunnable() {
@Override
publicvoidrun() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
newThread(newRunnable() {
@Override
publicvoidrun() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
newThread(newRunnable() {
@Override
publicvoidrun() {
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
}

}

运行结果:

复制代码

6. 使用 enum 枚举数据类型实现单例模式

枚举 enum 和静态代码块的特性相似,在使用枚举类时,构造方法会被自动调用,也可以应用其这个特性实现单例设计模式。

复制代码

public enum Singleton {
INSTANCE;
privateMyObject myObject = null;
Singleton(){
myObject =newMyObject();
}
public MyObject getInstance(){
return myObject;
}

public static void main(String[]args){
newThread(newRunnable(){
@Override
public void run(){
System.out.println(Singleton.INSTANCE.getInstance().hashCode());
}
}).start();
newThread(newRunnable(){
@Override
public void run(){
System.out.println(Singleton.INSTANCE.getInstance().hashCode());
}
}).start();
newThread(newRunnable(){
@Override
public void run(){
System.out.println(Singleton.INSTANCE.getInstance().hashCode());
}
}).start();
}
}

运行结果:

复制代码

这样实现的一个弊端就是违反了“职责单一原则”,完善后的代码如下:

public class MyObject {

public enum Singleton {

INSTANCE;

private MyObject myObject = null;

复制代码

Singleton(){
myObject =newMyObject();
}

public MyObject getInstance(){
return myObject;
}
}

public static MyObject getInstance(){
returnSingleton.INSTANCE.getInstance();
}
public static void main(String[]args){
newThread(newRunnable(){
@Override
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
newThread(newRunnable(){
@Override
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
newTh

复制代码

read(newRunnable(){
@Override
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}).start();
}

}

运行结果:

复制代码

7. 文末总结

本文使用若干案例来阐述单例模式与多线程结合遇到的情况与解决方案。

原文  https://www.infoq.cn/article/fb4zXeEzpTHirBS8Tel9
正文到此结束
Loading...