【多线程】单例模式

JavaEE专栏入口

文章摘要:

  • 单例模式是一种设计模式,确保类只有一个实例并提供全局访问点。饿汉模式在类加载时立即创建实例,实现简单且线程安全,但可能浪费资源。懒汉模式延迟实例创建,首次调用时才初始化,更节省资源但需处理线程安全问题。多线程环境下,懒汉模式通过双检锁机制(synchronized+volatile)保证线程安全。两种模式的核心区别在于初始化时机:饿汉模式立即创建,懒汉模式延迟创建。选择模式需权衡资源利用和线程安全需求。

一、单例模式是什么

单例模式是一种设计模式,设计模式是类似于棋谱、菜谱这类入门指南,相当于一种固定的套路/模板/公式,按照这个设计模式会方便很多。



单例模式是保证系统实例唯一性的重要手段

单例模式首先通过将类的实例化方法(即构造方法)私有化来防止程序通过其他方式创建该类的实例,然后通过提供一个全局唯一获取该类实例的方法(即getInstance方法)帮助用户获取类的实例,用户只需也只能通过调用该方法获取类的实例。

单例模式的设计保证了一个类在整个系统中同一时刻只有一个实例存在,主要被用于一个全局类的对象在多个地方被使用并且对象的状态是全局变化的场景下。同时,单例模式为系统资源的优化提供了很好的思路,频繁创建和销毁对象都会增加系统的资源消耗,而单例模式保障了整个系统只有一个对象能被使用,很好地节约了资源。

二、饿汉模式

饿汉模式指在类中直接定义全局的静态对象的实例并初始化,然后提供一个方法(getInstance方法)获取该实例对象。通过饿汉模式构造单例模式的代码如下:

Java 复制代码
class SingleTonHungry {
	// 只能存在这一个实例
	private static SingleTonHungry instance = new SingleTonHungry();

	// get方法
	public static SingleTonHungry getInstance() {
		return instance;
	}
	
	// 构造方法
	private SingleTonHungry() {
		// ......
	}
}

因为程序一启动就会创建实例并初始化,迫切需要使用实例,就像饿了很久的人见到美食迫切敞开肚子开吃一样,因此称为饿汉模式。

对饿汉模式分析,我们可以发现get方法只有一个return语句,这意味着只涉及到操作,不涉及到修改变量的操作,因此一般不会存在线程安全问题

三、懒汉模式

懒汉模式和饿汉模式是相对的,如果说饿汉模式是尽可能早地创建实例,那么懒汉模式就是尽可能晚地创建实例,在某些情况下也可能不创建实例(延迟创建)。

举个例子,用户在浏览网页的时候不会在短时间内看的了太多的内容,就只加载第一页的内容,这样既不会对内存有很大压力,也可以提高效率。当用户翻页,再加载后续的内容,这样动态地加载。

懒汉模式和饿汉模式的最大不同在于,懒汉模式在类中定义了单例但是并未实例化,实例化的过程是在获取单例对象的方法中实现的,也就是说,在第一次调用懒汉模式时,该对象一定为空,然后去实例化对象并赋值,这样下次就能直接获取对象了;而饿汉模式是在定义单例对象的同时将其实例化的,直接使用便可。

3.1 单线程环境下的懒汉模式

Java 复制代码
class SingleTonLazy {
	// 先将实例置空
	private static SingleTonLazy instance = null;

	// get方法
	public static SingleTonLazy getInstance() {
		// 判断是否第一次调用get方法
		if (instance == null) {
			instance = new SingleTonLazy();
		}
		return instance;
	}
	
	// 构造方法
	private SingleTonLazy() {
		// ......
	}
}

在以上的代码中,创建实例需要在第一次调用get方法的时候,后续如果再调用get方法,instance不为空就不会再new一个实例了。

3.2 多线程环境下的懒汉模式

我们来分析一下单线程版本的懒汉模式是否存在线程安全问题呢?

线程安全问题无非三个方面:

  • 原子性
  • 可见性
  • 有序性

原子性

get方法中的 "if条件判断语句" 和 "new创建实例语句" 这两条语句必须要是原子性的才不会出现bug,否则会出现问题:

  • if条件判断语句为步骤一,new创建实例语句为步骤二
  • 当t1线程执行了步骤一时,线程调度到t2线程了
  • 此时t2线程也执行了步骤一,线程调度回t1线程
  • t1线程继续执行步骤二创建了一个实例,接着线程调度到t2
  • t2线程也继续执行步骤二,也创建了一个实例
  • 创建了两个实例,不符合预期,出现线程安全问题

我们对get方法加个锁,强行绑定if语句和new语句,此时的synchronized的锁对象是类对象 SingleTonLazy.class:

Java 复制代码
class SingleTonLazy {
	// 先将实例置空
	private static SingleTonLazy instance = null;

	// get方法:加锁绑定if语句和new语句 
	public static synchronized SingleTonLazy getInstance() {
		// 判断是否第一次调用get方法
		if (instance == null) {
			instance = new SingleTonLazy();
		}
		return instance;
	}
	
	// 构造方法
	private SingleTonLazy() {
		// ......
	}
}

或者直接在if语句外面套一层synchronized加锁语句:

Java 复制代码
class SingleTonLazy {
	// 先将实例置空
	private static SingleTonLazy instance = null;
	private static Object locker = new Object();

	// get方法
	public static SingleTonLazy() {
		// 加锁绑定if语句和new语句
		synchronized (locker) {
			// 判断是否第一次调用get方法
			if (instance == null) {
				instance = new SingleTonLazy();
			}
		}
		return instance;
	}
	
	// 构造方法
	private SingleTonLazy() {
		// ......
	}
}

但是,程序运行并调用get方法后,每一次都会加锁,都会导致线程阻塞,这也会降低程序运行的效率。

为了提高程序的效率,我们可以再加一层if语句:

Java 复制代码
class SingleTonLazy {
	// 先将实例置空
	private static SingleTonLazy instance = null;
	private static Object locker = new Object();

	// get方法
	public static SingleTonLazy() {
		if (instance == null) {	// 判断是否需要加锁
			// 加锁绑定if语句和new语句
			synchronized (locker) {
				// 判断是否第一次调用get方法
				if (instance == null) {
					instance = new SingleTonLazy();
				}
			}
		}
		return instance;
	}
	
	// 构造方法
	private SingleTonLazy() {
		// ......
	}
}

这样一来,原子性的问题就解决了~

可见性和有序性

在代码中,new语句通常对应着三条指令:

  1. 申请内存空间
  2. 在内存空间上创建实例(初始化)
  3. 将内存空间的首地址赋值给引用变量

    一般来说,以上三条指令的执行顺序是 1 -> 2 -> 3,但是由于编译器优化,可能出现的执行顺序是:1 -> 3 -> 2:
  • 当线程执行1之后,就执行3
  • 接下来可能通过引用变量调用方法,但是此时并没有对实例进行初始化,就会出现问题

volatile关键字具备两种特性:一种是保证该变量对所有线程可见,在一个线程修改了变量的值后,新的值对于其他线程是可以立即获取的;一种是volatile禁止指令重排,即volatile变量不会被缓存在寄存器中或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

因此我们将实例用 volatile关键字修饰即可。

这样一来,可见性和有序性也解决了,这就是多线程环境下的懒汉模式的代码,这个处理线程安全问题的方法是双检锁,既可以提高程序效率减少因加锁产生的额外开销,又可以通过volatile保证禁止指令重排序。

代码如下:

Java 复制代码
class SingletonLazy {
	// 用volatile关键字修饰实例
    private static volatile SingletonLazy instance = null;
    private static Object locker = new Object();

    // get方法
    public static SingletonLazy getInstance() {
        if (instance == null) {
            synchronized (locker) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

    // 构造方法
    private SingletonLazy() {

    }
}

面试题:饿汉模式和懒汉模式的核心区别是什么?各自的优缺点?如何在多线程环境中保证懒汉模式的安全?


  • 在初始化时机方面:饿汉模式在类加载时立即创建单例实例,与线程是否调用无关;懒汉模式在首次调用getInstance方法时才创建单例实例,实现延迟加载。
  • 资源开销方面:饿汉模式的优点是初始化简单且无延迟加载问题、静态变量初始化由JVM保证,开销可控,缺点是资源可能会浪费,即使实例未被使用也会占用内存和CPU资源;懒汉模式的优点是资源高效,只在需要时才创建实例减少内存和启动开销,缺点是实现较复杂需要额外同步机制防止线程安全问题,增加了代码的复杂度。
  • 线程安全性方面:饿汉模式是天然线程安全的,静态变量instance在类加载期间由JVM初始化整个过程是原子的,无需额外同步;懒汉模式是非线程安全的,在原始实现下多个线程同时访问getInstance方法可能会导致创建多个实例,需要处理线程安全问题。
  • 多线程中保证懒汉模式安全的方法:双检锁 + volatile修饰实例。只有在首次调用getInstance方法时才会进行加锁操作,减少同步开销;利用volatile关键字禁止指令重排序,防止"部分初始化的对象"被访问

那今天到这里就告一段落了,若有错误请尽管指出!😊🌹


相关推荐
無森~2 小时前
HBase Java API
java·大数据·hbase
大尚来也2 小时前
看不见的加速器:深入理解 Linux 页缓存如何提升 I/O 性能
java·开发语言
zhougl9962 小时前
Java 常见异常梳理
java·开发语言·python
缘空如是2 小时前
基础工具之jsoup工具
java·jsoup·html解析
毕设源码-郭学长2 小时前
【开题答辩全过程】以 基于Nodejs的网上书店 为例,包含答辩的问题和答案
java·eclipse
you-_ling2 小时前
Linux软件编程:Shell命令
java·linux·服务器
数智工坊2 小时前
【数据结构-栈、队列、数组】3.3栈在括号匹配-表达式求值上
java·开发语言·数据结构
凌康ACG2 小时前
Warm-Flow国产工作流引擎入门
java·工作流引擎·flowable·warm-flow
知我心·2 小时前
Java 正则表达式知识点总结
java