单例模式中的线程安全(延迟加载)

设计模式中常用的单例模式,在jvm中可以保证该对象只有一个实例存在。这样对于一些特别大的对象,可以很好的节省资源。由于省去了new,所以节省了gc消耗。同时,对于一些核心系统逻辑,可以能要一个对象实例来控制会省去很多麻烦。

单例模式,如果不考虑多线程,则可以如下创建

public class Singleton {
	/* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */
	private static Singleton instance = null;
	/* 私有构造方法,防止被实例化 */
	private Singleton() {
	}
	/* 静态工程方法,创建实例 */
	public static Singleton getInstance() {
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
	/* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
	public Object readResolve() {
		return instance;
	}
}

上面提到延迟加载,所谓延迟加载,就是指当实际用到该对象时才加载对应类,否则只是声明,并未实际花费资源去初始化对象。

最好的理解方式,将上面代码中如下语句,此时为非延迟加载,加载该单例类时,也会初始化该静态类的静态成员变量,并调用new来创建实例。而采用延迟加载,则需要当调用getInstance()方法 时,才会通过new初始化实例。

private static Singleton instance = new Singleton();

以上是单例模式中延迟加载的解释,但是上面的示例是不考虑多线程下的单例模式。如果多线程下进行延迟加载,上述单利模式是否有问题? 答案是有。

如果多个线程同时调用getInstance,则可能会调用多次new,会产生问题。如何避免呢?我们很容易想到使用锁、synchronized等方法。

synchronized:

如下,该方法可以保证类每次调用getInstance时,都只有一个线程在使用。但是问题也来了,每次调用都会锁住这个对象,因为synchronized用在方法上时,锁住的是整个类对象(ps:如果一个对象有多个synchronized方法,某一时刻某个线程已经进入到了某个synchronized方法,那么在该方法没有执行完毕前,其他线程是无法访问该对象的任何synchronized方法的。注意这时候是给对象上锁,如果是不同的对象,则各个对象之间没有限制关系。但如果是静态方法的情况(方法加上static关键字),即便是向两个线程传入不同的对象(同一个类),这两个线程仍然是互相制约的,必须先执行完一个,再执行下一个)我们不希望这样子,因为这样子在并发处理中会损失性能。

public static synchronized Singleton getInstance() {
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}

根据上面,我们做了改动,将synchronized关键字放在代码块中,这样锁定的就不是整个对象,而是方法块,synchronized块比synchronized方法更加细粒度地控制了多个线程的访问,只有synchronized块中的内容不能同时被多个线程所访问,方法中的其他语句仍然可以同时被多个线程所访问(包括synchronized块之前的和之后的。如下

public static Singleton getInstance() {
		if (instance == null) {
			synchronized (instance) {
				if (instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}

这个时候,貌似解决了问题。但是,我想说的是,从网上看到的资料里面有如下问题情况:jvm优化使得new singleton()的操作实际上不是原子操作,包括分配内存和初始化对象两个过程,很有可能第一个线程进入分配内存后,就已经将instance分配内存地址,此时不为null,但是还没有分对象初始化,此时另一个线程发现instance不等于null,就直接开始使用对象的其他方法,就会报错,类似classnotdefine或者noclass之类的异常报错。

最后,不得不使用如下的代码解决:

private static class SingletonFactory{         
        private static Singleton instance = new Singleton();         
    }         
    public static Singleton getInstance(){         
        return SingletonFactory.instance;         
    }

以上为SingletonFactory为内部类,放在Singleton类中。JVM有个特性:一个类被加载时,该类是线程互斥的,且只会被加载一次。所以如上代码,当调用getInstance()时(该方法可以多线程同时访问),就可以实现加载类了(第一个线程访问就会加载SingletonFactory,并创建Class对象。其他线程要等他创建完成后才能访问SingletonFactory,且不会再new)。

以上方法可以解决问题,同时也有人参考上面的思路来做了另一种,因为上面主要就是让访问可以多线程同时,对象获取只能单线程互斥,那把static class SingletonFactory换成一个synchronized的方法应该也可以吧,如下代码:

public class SingletonTest {
	private static SingletonTest instance = null;
	private SingletonTest() {
	}
	private static synchronized void syncInit() {
		if (instance == null) {
			instance = new SingletonTest();
		}
	}
	public static SingletonTest getInstance() {
		if (instance == null) {
			syncInit();
		}
		return instance;
	}
}

可能一眼看起来和synchronized块的方法类似,这个是否也会发生类似的问题?(我的疑虑,不过貌似很难测者中情况,也许synchronized方法时会保证方法中的变量都创建对象并赋值)。

时间: 2024-10-15 07:09:36

单例模式中的线程安全(延迟加载)的相关文章

如何保证单例模式在多线程中的线程安全性

对大数据.分布式.高并发等知识的学习必须要有多线程的基础.这里讨论一下如何在多线程的情况下设计单例模式.在23中设计模式中单例模式是比较常见的,在非多线程的情况下写单例模式,考虑的东西会很少,但是如果将多线程和单例模式结合起来,考虑的事情就变多了,如果使用不当(特别是在生成环境中)就会造成严重的后果.所以如何使单例模式在多线程中是安全的显得尤为重要,下面介绍各个方式的优缺点以及可用性: 1.立即加载(饿汉模式) 立即加载模式就是在调用getInstance()方法前,实例就被创建了,例: pub

每天进步一点点——Linux中的线程局部存储(一)

转载请说明出处:http://blog.csdn.net/cywosp/article/details/26469435 在Linux系统中使用C/C++进行多线程编程时,我们遇到最多的就是对同一变量的多线程读写问题,大多情况下遇到这类问题都是通过锁机制来处理,但这对程序的性能带来了很大的影响,当然对于那些系统原生支持原子操作的数据类型来说,我们可以使用原子操作来处理,这能对程序的性能会得到一定的提高.那么对于那些系统不支持原子操作的自定义数据类型,在不使用锁的情况下如何做到线程安全呢?本文将从

用两个小例子来解释单例模式中的“双重锁定”

学习单例模式时,好多人都不太理解双重锁定.学完后突然想到一个很有趣的例子. 单例模式结构图: 代码: Singleton类 class Singleton { private static Singleton instance; private static readonly object syncRoot = new object(); //程序运行时创建一个静态只读的进程辅助对象 private Singleton() { } //用private修饰构造方法,防止外界利用new创建此类实例

Java中处理线程同步

引自:http://blog.csdn.net/aaa1117a8w5s6d/article/details/8295527和http://m.blog.csdn.net/blog/undoner/12849661 静态变量:线程非安全. 静态变量即类变量,位于方法区,为所有对象共享,共享一份内存,一旦静态变量被修改,其他对象均对修改可见,故线程非安全. 实例变量:单例模式(只有一个对象实例存在)线程非安全,非单例线程安全. 实例变量为对象实例私有,在虚拟机的堆中分配,若在系统中只存在一个此对象

c#lock语句及在单例模式中应用

C#中的lock语句是怎么回事,有什么作用? C#中的lock语句将lock中的语句块视为临界区,让多线程访问临界区代码时,必须顺序访问.他的作用是在多线程环境下,确保临界区中的对象只被一个线程操作,防止出现对象被多次改变情况. 注意的地方有:lock对象必须是一个不可变对象,否则无法阻止另一个线程进入临界区.最好是private static readonly 或者private static.常见的lock (this).lock (typeof (MyType)) 和 lock ("myL

线程池中的线程的排序问题

1 package org.zln.thread.poolqueue; 2 3 import org.slf4j.Logger; 4 import org.slf4j.LoggerFactory; 5 6 import java.util.Comparator; 7 import java.util.UUID; 8 import java.util.concurrent.*; 9 10 /** 11 * 线程池中的线程的排序问题 12 * Created by sherry on 16/11/4

QT中的线程与事件循环理解(2)

1. Qt多线程与Qobject的关系 每一个 Qt 应用程序至少有一个事件循环,就是调用了QCoreApplication::exec()的那个事件循环.不过,QThread也可以开启事件循环.只不过这是一个受限于线程内部的事件循环.因此我们将处于调用main()函数的那个线程,并且由QCoreApplication::exec()创建开启的那个事件循环成为主事件循环,或者直接叫主循环.注意,QCoreApplication::exec()只能在调用main()函数的线程调用.主循环所在的线程

分布式系统中的线程与进程

进程 虽然进程构成了分布式系统中的基本组成单元,但是操作系统提供的用于构建分布式系统的进程在粒度上还是太大了,而就粒度而言,将每个进程细分为若干控制线程的形式则更加合适. 为了程序执行的需要,操作系统创建多个虚拟处理器,每个虚拟处理器运行一个程序.为了保持对这些虚拟处理器的跟踪,操作系统中有一张进程表.其包含的条目中存储着CPU寄存器值.内存映像.打开的文件.统计信息.特权信息等. 操作系统特别注意确保独立的进程不会有意或无意地破坏其他独立进程运行的正确性.也就是说,多个进程并发地共享同一个CP

Java中的线程池

综述 在我们的开发中经常会使用到多线程.例如在Android中,由于主线程的诸多限制,像网络请求等一些耗时的操作我们必须在子线程中运行.我们往往会通过new Thread来开启一个子线程,待子线程操作完成以后通过Handler切换到主线程中运行.这么以来我们无法管理我们所创建的子线程,并且无限制的创建子线程,它们相互之间竞争,很有可能由于占用过多资源而导致死机或者OOM.所以在Java中为我们提供了线程池来管理我们所创建的线程. 线程池的使用 采用线程池的好处 在这里我们首先来说一下采用线程池的