如何书写线程安全的单例实现?

如何书写线程安全的单例实现?

       在某些场景下只需要初始化一个对象一次,在后面需要使用的时候直接获取之前初始化的对象,这可以借助单例模式编程模帮助我们来实现这个需求,常见的单例模式有饿汉式和懒汉式的实现方式,还有通过枚举的方式实现单例,今天就将单例模式的实现进行简单的总结并就其线程安全性进行浅析,有不妥的地方还请大家批评指正。所有代码实例皆以JAVA进行实现。

一、懒汉式实现

private SingleronExample2(){} // 提供私有的构造方法防止在外部实例化
	private static SingleronExample2 instance = null;
	public static SingleronExample2 getInstance(){
		if (instance == null){ // 判断是否已经初始化
			instance = new SingleronExample2();
		} 
		return instance;
	}

        以上是懒汉式核心代码,懒汉式顾名思义,在需要的时候才初始化需要的目标对象,懒汉式的实现是线程不安全的,来看看线程不安全的原因,在多线程并发访问的情况下,多个线程都来获取单例对象(第一次获取),同时运行到了if (instance == null)这段代码,发现instance == null然后两个访问都会执行new SingleronExample2();这就实例化两个对象,无法满足单例的需求。

那么单例模式是否可以通过改进实现线程安全呢,答案是肯定的,可以通过加锁的方式来实现懒汉式的线程安全,下面就来看一下一个实现。

private SingleronExample5(){}
	private static SingleronExample5 instance = null;
	private static SingleronExample5 getInstance(){
if (instance == null){ 
			synchronized(SingleronExample5.class){
				if (instance == null){
					instance = new SingleronExample5();
				}
			}
		} 
		return instance;
	}

       来看看上面这段代码,通过synchronized关键字对SingleronExample5.class加锁,使用双重检测机制判断单例对象是否已经实例化,加锁后在同一个时刻只能有一个线程获取锁,并执行加锁代码块中的内容进行对象实例化,这样可以保证在大部分情况下的单例对象不会被重复初始化,但仍然不是完全线程安全的,来看看是什么原因呢?

这就需要谈到JVMcpu的指令重排序机制,编译源代码时,编译器依据对上下文的分析,对指令进行重排序,以更适合于CPU的并行执行。一个对象实例化的步骤为:

1memory = allocate()分配内存空间

3instanance = memory 设置instance纸箱刚刚分配的内存

2ctorInstannce初始化对象

在初始化时如果一个线程刚好执行了(3)后另外一个线程立马走到了第一个检测的位置:if (instance == null),此时instance != null,便直接返回了instance而此时的instance==null。并没有被实例化完成。

我们可以统一个关键字来修改单例对象volatilejava虚拟机中对volatile关键字描述的是:(1)内存可见性;(2)禁止指令重排序

什么是内存可见性? 我们知道对于普通的非volatile的变量,CPU每次在使用时会从内存中拷贝一份到CPU cache中,对变量的操作(读、写)都是操作的拷贝后的副本,然后再同步到内存中,而对于被volatile修饰的对象如果你状态或者值发生变更,能够立马被其它线程所看见,volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,保证了每次读写变量都从主内存中读,跳过CPU cache这一步。当一个线程修改了这个变量的值,新值对于其他线程是立即得知的。

禁止指令重排序?前面提到了编译源代码时,编译器依据对上下文的分析,对指令进行重排序,以更适合于CPU的并行执行。被volatile修饰的变量在编译时其实例化的指令不会被重排序。

所以将单例对象再使用volatile修饰后便可以保证饿汉实现的方式线程安全了。

private SingleronExample5(){}
	private static volatile SingleronExample5 instance = null;
	private static SingleronExample5 getInstance(){
if (instance == null){ 
			synchronized(SingleronExample5.class){
				if (instance == null){
					instance = new SingleronExample5();
				}
			}
		} 
		return instance;
	}

        加锁出了使用synchronized(虚拟机级别的锁,JVM来帮助我们是加锁和解锁机制,方便好用)还可以使用ReentrantLock来实现加锁和解锁。

    private SingleronExample5(){}
    prvate static Lock lock = new ReentrantLock();
	private static volatile SingleronExample5 instance = null;
	private static SingleronExample5 getInstance(){
    if (instance == null){ 
				lock.lock(); // 加锁
				try { if (instance == null){
					instance = new SingleronExample5();
					}
               } finally {
               Lock.unlock(); // 解锁
              }
			}
		} 
		return instance;
	}

二、饿汉式实现

private SingleronExample1(){}
	private static SingleronExample1 instance = new SingleronExample1();
	public static SingleronExample1 getInstance(){
		return instance;
	}

上面的代码便是饿汉式的单例实现,从实现上来看非常的简单,可以看到饿汉式采用的是将类加载时就将到单例对象实例化了,在需要使用时直接返回类加载时实例化的单例对象,它是线程安全的。

三、过枚举的方式实现单例   

private SingleronExample4(){}
	public static SingleronExample4 getInstance(){
		return Singleton.INSTANCE.getSingleton();
	}
	private enum Singleton{
		INSTANCE;
		private SingleronExample4 singleton;
		// JVM会保证这个方法绝对只是被调用一次
		Singleton(){
			singleton = new SingleronExample4();
		}
		public SingleronExample4 getSingleton() {
			return singleton;
		}
}

枚举的实现仅给出一个代码实现,枚举的构造函数Singleton()JVM保证其只会被执行一次。

<think>好的,用户问的是synchronized能不能解决线程的有序性问题。首先,我得回忆一下Java中的synchronized关键字的作用。它主要用于解决线程的互斥和可见性问题,对吧?互斥是通过锁机制保证同一时间只有一个线程执行临界区代码,可见性则是因为synchronized会强制将工作内存中的变量刷新到主内存,确保其他线程能看到最新的值。 那有序性呢?有序性问题主要是由于指令重排序引起的。Java内存模型(JMM)允许编译器和处理器进行指令重排序,以提高性能,但在线程下会保证结果的一致性。不过多线程环境下,重排序可能导致不可预料的结果。synchronized在这方面的作用是怎样的呢? 根据JMM的规定,synchronized块的进入和退出会建立happens-before关系。也就是说,在释放锁之前的所有操作,对下一个获取同一个锁的线程来说是可见的,并且这些操作不会被重排序到锁释放之后。同样地,获取锁之后的操作不会重排序到锁获取之前。这样一来,synchronized确实可以在一定程度上限制指令重排序,从而保证有序性。不过,这种限制仅限于synchronized块内部的操作,对于块外的代码,或者不涉及共享变量的操作,synchronized可能无法完全阻止重排序。 不过,我之前学过的内容中,可能有序性的保证更多依赖于volatile关键字或者显式的锁机制,比如Lock接口的实现类。volatile通过内存屏障来禁止重排序,而synchronized虽然能通过happens-before规则来保证有序性,但可能不如volatile直接。如,模式中的双重检查锁定问题,就需要用volatile来确保初始化对象的操作不会被重排序,这时候仅用synchronized是不够的。 所以,总结起来的话,synchronized确实可以在一定程度上解决有序性问题,因为它通过happens-before规则限制了指令重排序。但它的主要作用还是互斥和可见性,有序性的保证可能不如其他机制全面。需要根据具体的场景来选择,如果是有严格的有序性要求,可能需要结合volatile或者其他的同步机制。</think>synchronized关键字可以部分解决线程有序性问题,但需结合具体场景分析。以下是分步说明: 1. **有序性问题本质** 线程有序性问题源于: - 编译器指令重排序 - CPU乱序执行优化 - 内存系统的重排序 这些优化可能导致代码执行顺序与程序书写顺序不一致。 2. **synchronized的有序性保证** 通过建立happens-before规则实现: - 解锁操作happens-before后续对同一锁的加锁操作 - 同步块内的指令不会被重排序到同步块外 - 保证同一锁保护的临界区内代码的有序执行 3. **具体限制** ```java synchronized(lock) { // 代码块A int a = x; // 操作1 int b = y; // 操作2 // 代码块B } ``` - 操作1和操作2不会被重排序到同步块外 - 块内指令仍可能被重排序(只要不改变线程执行结果) - 不同同步块之间无法保证有序性 4. **对比volatile** | 特性 | synchronized | volatile | |-------------------|--------------|----------| | 原子性 | ✅ | ❌ | | 可见性 | ✅ | ✅ | | 完全禁止重排序 | ❌ | ✅ | | 有序性保证范围 | 同步块内 | 变量访问 | 5. **典型使用场景** - 需要同时保证原子性+可见性+基本有序性时 - 适合复合操作的同步控制(如:i++) - 不适合解决: $$ \text{跨多个同步块的有序性} \quad \text{或} \quad \text{非竞争资源的顺序控制} $$ 6. **注意事项** - 模式中双重检查锁定仍需配合volatile: ```java private static volatile Singleton instance; ``` - 同步块应尽量小以提高性能 - 锁对象的选择需避免不必要的竞争 结论:synchronized通过happens-before规则提供有限的有序性保证,适合解决与共享数据修改相关的有序性问题,但不能完全替代volatile或显式内存屏障对特定顺序的控制。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值