单例模式与多线程

一、前言

如何使单例模式遇到多线程是安全的、正确的?

我们在学习设计模式的时候知道单例模式有懒汉式和饿汉式之分。简单来说,饿汉式就是在使用类的时候已经将对象创建完毕,懒汉式就是在真正调用的时候进行实例化操作。

二、饿汉式+多线程

单例:

public class MyObject {
    //饿汉模式
	
	private static MyObject myObject=new MyObject();
	private MyObject(){
		
	}
	public static MyObject getInstance(){
		return myObject;
	}
}

自定义线程:

public class MyThread extends Thread {
	@Override
	public void run(){
		System.out.println(MyObject.getInstance().hashCode());
	}
}

main方法:

public class Run {
 
	public static void main(String[] args) {
		MyThread t1=new MyThread();
		MyThread t2=new MyThread();
		MyThread t3=new MyThread();
		t1.start();
		t2.start();
		t3.start();
 
	}
 
}

结果:

hashCode是同一个值,说明对象是同一个。也就是说饿汉式单例模式在多线程环境下是线程安全的。

三、懒汉式+多线程

方案一:

单例:

public class MyObject {
	private static MyObject myObject;
	/*私有构造函数避免被实例化*/
	private MyObject(){
		
	}
	public static MyObject getInstance(){
		try {
			if (myObject==null) {
				//模拟在创建对象之前做的一些准备性工作
				Thread.sleep(3000);
				myObject=new MyObject();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return myObject;
	}
 
}

自定义线程:

public class MyThread extends Thread {
  @Override
  public void run(){
	  System.out.println(MyObject.getInstance().hashCode());
  }
}

main方法:

public class Run {
 
	public static void main(String[] args) {
		MyThread t1=new MyThread();
		MyThread t2=new MyThread();
		MyThread t3=new MyThread();
		t1.start();
		t2.start();
		t3.start();
 
	}
 
}

结果:

3种hashCode,说明创建出了3个对象,并不是单例的。懒汉模式在多线程环境下是“非线程安全”。这是为何?

因为创建实例对象的那部分代码没有加synchronized或Lock。三个线程都进入了创建实例对象的代码段getInstance。

方案二:synchronized同步方法

既然多个线程可以同时进入getInstance()方法,那么只需要对getInstance()方法声明synchronized关键字即可。在MyObject的getInstance()方法前加synchronized关键字。最终打印的三个hashcode是一样一样的。实现了多线程环境下,懒汉模式的正确性、安全性。但是此种方法的运行效率非常低下,因为是同步的,一个线程释放锁之后,下一个线程继续执行。
方案三:synchronized同步代码块

同步方法是对方法整体加锁,效率不高,我们可以通过减少锁的粒度,也就是使用synchronized同步代码块。如下面代码所示:

public class MyObject {
	private static MyObject myObject;
	/*私有构造函数避免被实例化*/
	private MyObject(){
		
	}
	/*synchronized*/
	public  static MyObject getInstance(){
		try {
			synchronized (MyObject.class) {
				if (myObject==null) {
					//模拟在创建对象之前做的一些准备性工作
					Thread.sleep(3000);
					myObject=new MyObject();
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return myObject;
	}
 
} 

这样做能保证最终运行结果正确,但getInstance方法中的全部代码都是同步的了,这样做会降低运行效率,和对getInstance方法加synchronized的效率几乎一样。

方案四:重要代码同步代码块

public class MyObject {
	private static MyObject myObject;
	/*私有构造函数避免被实例化*/
	private MyObject(){
		
	}
	/*synchronized*/
	public  static MyObject getInstance(){
		try {
			
			if (myObject==null) {
				//模拟在创建对象之前做的一些准备性工作
				Thread.sleep(3000);
				synchronized (MyObject.class) {
				myObject=new MyObject();
				}
			}
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		return myObject;
	}
 
}

结果:

这种做法在多线程环境下还是无法解决得到同一个实例对象的结果。

方案五:双重锁定

package singleton_3;
 
public class MyObject {
	private static MyObject myObject;
	/*私有构造函数避免被实例化*/
	private MyObject(){
		
	}
	//使用双重锁定(Double-Check Locking)解决问题,既保证了不需要同步代码的异步执行性,
	//又保证了单例的效果
	public static MyObject getInstance(){
		try {
			if (myObject==null) {
				//模拟在创建对象之前做一些准备性的工作
				Thread.sleep(3000);
				synchronized(MyObject.class){
					if (myObject==null) {
						myObject=new MyObject();
					}
				}
				
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return myObject;
	}
 
}

使用双重锁定功能,成功地解决了在多线程环境下“懒汉模式”的“非线程安全”问题。
那么为什么外面已经判断myObject实例是否存在,为什么在lock里面还需要做一次myObject实例是否存在的判断呢?

如果myObject已经存在,则直接返回,这没有问题。当Instance为null,并且同时有3个线程调用GetInstance()方法时,它们都可以通过第一重myObjectnull的判断,然后由于lock机制,这三个线程只有一个进入,另外2个在外排队等候,必须第一个线程走完同步代码块之后,第二个线程才进入同步代码块,此时判断instancenull,为false,直接返回myObject实例。就不会再创建新的实例啦。第二个监测myObject==null一定要在同步代码块中。
方案六:

方案五表面上来看,在执行该代码时,先判断instance对象是否为空,为空时再进行初始化对象。即使是在多线程环境下,因为使用了synchronized锁进行代码同步,该方法也仅仅创建一个实例对象。但是,从根本上来说,这样写还是存在一定问题的。 问题源头:

创建对象:1.创建对象时限分配内存空间-----》2.初始化对象-----》3.设置对象指向内存空间-----》4.初次访问对象;

2和3可能存在重排序问题,由于单线程中遵守intra-thread semantics,从而能保证即使2和3交换顺序后其最终结果不变。但是当在多线程情况下,线程B将看到一个还没有被初始化的对象,此时将会出现问题。

解决方案:

1、不允许②和③进行重排序

2、允许②和③进行重排序,但排序之后,不允许其他线程看到。

基于volatile的解决方案

对前面的双重锁实现的延迟初始化方案进行如下修改:

public class MyObject {
	private volatile static MyObject myObject;

	/* 私有构造函数避免被实例化 */
	private MyObject() {

	}

	/* synchronized */
	public static MyObject getInstance() {
		try {

			if (myObject == null) {
				// 模拟在创建对象之前做的一些准备性工作
				Thread.sleep(3000);
				synchronized (MyObject.class) {
					if (myObject == null) {
						myObject = new MyObject(); // 用volatile修饰,不会再出现重排序
					}
				}
			}

		} catch (Exception e) {
			e.printStackTrace();
		}
		return myObject;
	}

}

使用volatile修饰instance之后,之前的②和③之间的重排序将在多线程环境下被禁止,从而保证了线程安全执行。
注意:这个解决方案需要JDK5或更高版本(因为从JDK5开始使用新的JSR-133内存模型规范,这个规范增强了volatile的语义)

基于类初始化的解决方案

JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。基于这个特性,可以实现另一种线程安全的延迟初始化方案。

public class MyObject {
	private static MyObject myObject;
	/*私有构造函数避免被实例化*/
	private MyObject(){
		
	}
	//静态内部类方式
	private static class MyObjectHandler{
		private static MyObject myObject=new MyObject();
	}
	public static MyObject getInstance(){
		return MyObjectHandler.myObject;
	}
}

结果可行。

使用静态代码块实现单例模式

public class MyObject {
	private static MyObject instance;
	/*私有构造函数避免被实例化*/
	private MyObject(){
		
	}
	static{
		instance=new MyObject();
	}
	public static MyObject getInstance(){
		return instance;
	}
}

结果可行。该方案的实质是,允许②和③进行重排序,但不允许非构造线程(此处是B线程)“看到”这个重排序。

四、总结

单例模式分为懒汉式和饿汉式,饿汉式在多线程环境下是线程安全的;懒汉式在多线程环境下 是“非线程安全”的,可以通过synchronized同步方法和“双重检测”机制来保证懒汉式在多线程环境下的线程安全性。静态内部类实现单例模式和静态代码块从广义上说都是饿汉式的。

在这里插入图片描述
在这里插入图片描述

博主介绍:上海交大毕业,大厂资深Java后端工程师,
《Java全套学习资料》作者,
专注于系统架构设计和高并发解决方案
阿里云开发社区乘风者计划专家博主,
CSDN平台Java领域优质创作者
常年分享Java技术干货、项目实战经验,
并为大学生和初学者提供项目实战与就业指导

擅长:分布式系统、SpringCloud、SpringBoot、Vue、MySQL、Redis、Docker等项目开发和设计

/**
 * @author[vx] vip1024p(备注java)
 * @【描述:浏览器打开】docs.qq.com/doc/DUkVoZHlPeElNY0Rw
 */
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello!!!");
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值