双重检查锁为什么要使用volatile字段

双重检查锁的由来

在单例模式中,有一个DCL(双重锁)的实现方式,在Java程序中,很多时候需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象的时候才进行开始初始化。

先来看下面实现单例的方式:

非线性安全的延迟初始化对象方式:

public class Test1 {
    private static SingletonInstance instance;
    private Test1(){}
    public static SingletonInstance getInstance(){
        if (null == instance){
            instance = new SingletonInstance();
        }
        return instance;
    }
}

        上面的这种实现方式在高并发的环境下是有问题的,我们可以对getInstance方法做同步处理来实现线性安全,如下:

public class Test1 {
    private static SingletonInstance instance;
    private Test1() { }
    public synchronized static SingletonInstance getInstance(){
        if (null == instance){
            instance = new SingletonInstance();
        }
        return instance;
    }
}

       但是这种同步方式会导致性能的开销,若getInstance被多个线程频繁调用,这将会导致程序执行性能的下降。只有在线程调用不多的场景下才可以,性能的开销可以忽略不计。

       基于上述的问题,后来有人提出来了双重检查(Double-Checked Locking)的方法,通过双重检查来降低同步带来的性能损耗,如下:

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

乍一看,是很完美的解决了损耗问题,但是这种做法是错误的。

在line7 :  instance = new SingletonInstance();创建单例对象的时候可以分解为下面三行伪代码:

//1、为对象分配内存空间
memory = allocation(); 
//2、初始化对象
initInstance(memory);
//3、设置instance指向刚刚分配的内存空间地址
instance = memory;

在JIT等编译的时候2-3可能会被重排,如重排后的结果如下:

//1、为对象分配内存空间
memory = allocation(); 
//3、设置instance指向刚刚分配的内存空间地址
instance = memory;
//2、初始化对象
initInstance(memory);

因此例如在line4的检查的时候instance可能还没有完全初始化好。这也导致了问题的根源所在。

为了解决重排的问题,我们就可以使用volatile关键字,来保证。

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

另外除了使用volatile关键字之外,还可以使用静态内部类的方式实现线程安全的单例,如下:

public class StaticClassInstance {
    private StaticClassInstance(){}
    private static class InstanceHandler{
        private static SingletonInstance instance =  new SingletonInstance();
    }
    public static SingletonInstance getInstance(){
        return InstanceHandler.instance; //在此处会使InstanceHandler类被初始化
    }
}

这种方式是基于JVM在类的初始化阶段(加载完成后并且未被线程使用之前),会执行类的初始化,在执行类的初始化期间,JVM会去获取一个锁,该锁可以同步多个线程对同一个类的初始化。

其实双重检查锁定(DCL)模式经常会出现在一些框架源码中,目的是为了延迟初始化变量。这个模式还可以用来创建单例。下面来看一个 Spring 中双重检查锁定的例子。

public class DefaultNamespaceHandlerResolver implements NamespaceHandlerResolver {
    
	/** Stores the mappings from namespace URI to NamespaceHandler class name / instance. */
	@Nullable
	private volatile Map<String, Object> handlerMappings;

	/**
	 * Load the specified NamespaceHandler mappings lazily.
	 */
	private Map<String, Object> getHandlerMappings() {
		Map<String, Object> handlerMappings = this.handlerMappings;
		if (handlerMappings == null) {
			synchronized (this) {
				handlerMappings = this.handlerMappings;
				if (handlerMappings == null) {
					if (logger.isTraceEnabled()) {
						logger.trace("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
					}
					try {
						Properties mappings =
								PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
						if (logger.isTraceEnabled()) {
							logger.trace("Loaded NamespaceHandler mappings: " + mappings);
						}
						handlerMappings = new ConcurrentHashMap<>(mappings.size());
						CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
						this.handlerMappings = handlerMappings;
					}
					catch (IOException ex) {
						throw new IllegalStateException(
								"Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
					}
				}
			}
		}
		return handlerMappings;
	}



}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
-多个线程的几种实现方式包括:承Thread类,实现Runnable接口,实Callable接口,使用线程池。 - Java中的线程池是通过ThreadPoolExecutor类实现的。线程池维护了一个线程队列,可以复用线程,减少线程的创建和销毁开销,提高了性能。 - 不建议直接使用Executors工具类创建线程池是因为它使用的是默认的线程池配置,可能导致线程数量过多,耗尽系统资源。OOM(Out of Memory)是由于创建过多的线程导致内存不足而发生的错误。 - Java内存模型(JMM)是一种规范,定义了多线程程序中各个变量的访问方式。它包括主内存和工作内存,通过控制变量的可见性和原子性来保证线程间的通信与同步。 - 并发编程可能会发生的问题包括:竞态条件、死、活、饥饿等。可见性问题指一个线程对共享变量的修改对其他线程是否可见,原子性问题指一个操作是否可以被中断或者同时执行。 - 并发编程下会出现原子性问题是因为多个线程同时修改同一个共享变量时,可能会导致不一致的结果。有序性问题是指程序执行的顺序与预期不符。可以使用synchronized关键字、Lock等来解决原子性和有序性问题。加上volatile关键字可以保证可见性,禁止指令重排序。 - 内存屏障是通过编译器和处理器来实现的,用于控制指令的执行顺序和内存的可见性。synchronized关键字会在进入和退出临界区时加上内存屏障。 - 单线程指令重排在不影响单线程执行结果的前提下进行优化,但可能会影响多线程的正确性。双重校验使用volatile是为了禁止指令重排,确保多线程环境下的正确性。 - InnoDB的索引是通过B+树实现的。B+树具有树高度低、查询效率高、支持范围查询等优势。 - 聚簇索引与非聚簇索引的区别在于数据的存储方式。聚簇索引将数据行存储在叶子节点中,非聚簇索引则将叶子节点指向数据行。不是所有情况都需要取回表的数据,可以通过覆盖索引来避免回表操作。 - 最左前缀匹配指在使用联合索引时,只有从左到右使用索引的前缀部分才能发挥索引的作用。将区分度高的字段放在最左边可以提高索引的效率。唯一索引与普通索引的区别在于是否允许重复值。 - 排查慢SQL可以通过查看慢查询日志、使用性能分析工具(如EXPLAIN、SHOW PROFILE)、优化查询语句等方法。 - MySQL的包括行和表。行在并发性能上更好,但需要更多的系统资源,适合处理并发访问较高的场景。表在资源消耗上较少,但并发性能相对较差,适合处理并发访问较低的场景。 - FOR UPDATE语句会对查询到的行加上行。 - 悲观是指在操作数据时始终假设会发生并发冲突,因此会将数据加以阻止其他事务的访问。乐观是指不加,而是通过版本号或时间戳等机制来判断是否发生冲突,减少了加的开销。悲观适用于并发冲突较多的场景,乐观适用于并发冲突较少的场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值