推荐看点
单例的使用场景很多,如可以通过单例实现应用缓存,这样多个线程统一对一块内存进行读写数据,既保障了数据的唯一性,又提高了业务处理性能。又比如使用单例实现应用的全局配置管理,保障全局有且仅有一个配置管理对象。
单例模式简介
单例是设计模式使用最为普遍的模式之一。它是一种对象创建模式,用于产生一个对象的具体实例,它可以确保系统中(单进程),一个类只产生一个实例。它的优势在于:
-
对于频繁使用的对象,可以省略new操作花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
-
由于new操作的次数减少,因此对系统内存的使用频率也会降低,这样将减少GC压力。
严格来说单例模式与并行没有直接的关系,是因为它太常见了,在应用开发的过程中将不可避免的会在多线程环境中使用到它。
创建一个单例类
由于单例类需要在多个模块间使用,因此单例类的本质是一个Sendable共享对象,创建一个单例类型最常用的有饿汉式和懒汉式,除此之外还有双重检查模式(这个不在本文中介绍)
注意:单例类需要使用 “use shared” 指令来标记,"use shared"需写在import语句之后,其他语句之前。
饿汉式
饿汉式:此种方式在类加载时,静态实例instance就已经创建并初始化好了。(这种方式当前有BUG,待BUG修复后删除此说明)
"use shared"
@Sendable
export class SingletonClassE {
static instance: SingletonClassE = new SingletonClassE();
private constructor() {
}
public doSomething() {
}
}
//使用方式
function doSomething() {
SingletonClassE.instance.doSomething()
}
此处没有将instance申明为private类型并提供getInstance静态方法,得益于ArkTS的语法校验,外界无法通过SingletonClass.instance = null/undefined/new SingletonClass()的方式给instance实例赋值。
根据使用习惯,也可以将instance申明为private类型,并提供静态方法getInstance()获取instance实例。
"use shared"
@Sendable
export class SingletonClassE {
private static instance : SingletonClassE = new SingletonClassE();
private constructor() {
}
public static getInstance() : SingletonClassE {
if (!SingletonClassE.instance) {
SingletonClassE.instance = new SingletonClassE();
}
return SingletonClassE.instance;
}
public doSomething() {
}
}
//使用方式
function doSomething() {
SingletonClassE.getInstance().doSomething()
}
-
优点
单例对象在创建时是线程安全的。
获取单例对象时不需要加锁。 -
缺点
类加载时即创建对象,无法实现懒加载。
一般认为延迟加载可以节省内存资源。但是延迟加载是不是真正的好,要看实际的应用场景,而不一定所有的应用场景都需要延迟加载。
懒汉式
懒汉式:将对象的创建延迟到了获取对象的时候,但为了线程安全,不得不为获取对象的操作加锁,这就导致了低性能。由于ArkTS提供的是异步锁,因此使用单例对象时,案例中提供了两种懒汉式单例的获取方式:
-
异步方式使用getInstanceAsync调用方需要使用async/await的方式,或者promise/then的方式。下方代码有说明。
-
同步方式使用getInstanceSync调用方在单例调用之前,需要先执行initSingletonClass。执行时机可以在自定义组件的aboutToAppear()中,或者在Ability的onCreate()中初始化。后续案例有介绍。如果initSingletonClass中有较长时间的执行逻辑,不建议使用这一套方案,因为会有调用getInstanceSync时,实例还未初始化好的风险
import utils from '@arkts.utils';
import { ArkTSUtils } from '@kit.ArkTS';
"use shared"
@Sendable
export class LazySingletonClass {
private static instance: LazySingletonClass;
private constructor() {
}
public static async getInstanceAsync() : Promise<LazySingletonClass> {
let lock: utils.locks.AsyncLock = utils.locks.AsyncLock.request('SingletonClass');
return lock.lockAsync(() => {
if (!LazySingletonClass.instance) {
LazySingletonClass.instance = new LazySingletonClass()
}
return LazySingletonClass.instance;
}, ArkTSUtils.locks.AsyncLockMode.EXCLUSIVE)
}
public static async initSingletonClass() {
let lock: utils.locks.AsyncLock = utils.locks.AsyncLock.request('SingletonClass');
return lock.lockAsync(() => {
if (!LazySingletonClass.instance) {
LazySingletonClass.instance = new LazySingletonClass()
}
}, ArkTSUtils.locks.AsyncLockMode.EXCLUSIVE)
}
public static getInstanceSync() {
return LazySingletonClass.instance
}
public doSomething() {
}
}
function doSomething() {
LazySingletonClass.getInstance().then((instance : LazySingletonClass) => {
instance.doSomething();
})
}
async function doSomethingAsync() {
(await LazySingletonClass.getInstance()).doSomething();
}
-
优点:
对象的创建是线程安全的。
支持延迟加载。
-
缺点:
获取对象的操作被加上了锁,影响了并发度。如果单例对象需要频繁使用,那这个缺点就是无法接受的。
由于异步锁的原因,需要在async方法中调用。
通过单例实现跨线程缓存
这是一个简单的例子,模拟单例实现内存管理的业务场景,总计15个线程分别进行增加和查询的操作,数据存储在单例对象LazySingletonClass中。
锁的使用
单例只是作为缓存的管理对象,因此提供公用缓存和基础接口(get/set),因此为了保证数据临界区的安全,锁需要在业务中进行管理(increaseNumber/queryNumber)
此处需要注意的是在increaseNumber中需要对数据操作的部分进行加锁,以保证数据的正确性。否则increase5、increase7、increase9在进入线程后,由于线程运行的时序不受控,因此通过getNum方法获取的指是不可预期的,最终输出无法达到预期。
案例中queryNumber也加了锁,是为了实现读写的互斥操作,模拟数据修改过程中不允许读取的业务场景(避免读取到脏数据)。
调用方式
-
异步调用:queryNumber线程中,使用的是异步调用(await LazySingletonClass.getInstanceAsync()).getNum()
-
同步调用在aboutToAppear中调用LazySingletonClass.initSingletonClass();对单例进行初始化在increase线程中,使用的是同步调用LazySingletonClass.getInstanceSync().setNum(increaseNum);
开发者可以结合自己的编程习惯选择采用同步或者异步方式。
while循环是为了模拟函数耗时。
import { LazySingletonClass } from './LazySingletonClass';
import utils from '@arkts.utils';
@Component
export struct SingletonPage {
aboutToAppear(): void {
LazySingletonClass.initSingletonClass();
}
build() {
NavDestination() {
Column() {
Text('单例模式')
Button('单例测试').onClick(event => {
console.info("==== onclick")
for (let i =0; i < 15; i++) {
if (i == 5 || i == 7 || i == 9) {
let increaseTask = new taskpool.Task("increase" + i, increaseNumber, "increase" + i)
taskpool.execute(increaseTask)
} else {
let queryTask = new taskpool.Task("query" + i, queryNumber, "query" + i)
taskpool.execute(queryTask)
}
}
})
}
.justifyContent(FlexAlign.SpaceEvenly)
.height('100%')
.width('100%')
}.hideTitleBar(true)
}
}
@Concurrent
export function queryNumber(taskName : string) {
let lock: utils.locks.AsyncLock = utils.locks.AsyncLock.request('numberlock');
return lock.lockAsync(async () => {
let start : number;
start = Date.now();
while (Date.now() - start < 100) {
// 模拟等待0.1秒
}
console.info("==== " + taskName + ' number is ' + (await LazySingletonClass.getInstanceAsync()).getNum())
}, ArkTSUtils.locks.AsyncLockMode.EXCLUSIVE)
}
@Concurrent
export function increaseNumber(taskName : string) {
console.info(taskName)
let lock: utils.locks.AsyncLock = utils.locks.AsyncLock.request('numberlock');
console.info("==== 1 " + taskName)
return lock.lockAsync(async () => {
console.info("==== 2 " + taskName)
let nu : number = LazySingletonClass.getInstanceSync().getNum();
console.info("==== " + taskName + " start increase " + nu)
let start = Date.now();
while (Date.now() - start < 100) {
// 模拟等待1秒
}
let increaseNum : number = nu + 5;
LazySingletonClass.getInstanceSync().setNum(increaseNum);
console.info("==== " + taskName + " end increase " + LazySingletonClass.getInstanceSync().getNum())
}, ArkTSUtils.locks.AsyncLockMode.SHARED)
}
单例类如下
import utils from '@arkts.utils';
import { ArkTSUtils } from '@kit.ArkTS';
"use shared"
@Sendable
export class LazySingletonClass {
private static instance: LazySingletonClass;
private num : number = 0;
private constructor() {
}
public static async getInstance(): Promise<LazySingletonClass> {
let lock: utils.locks.AsyncLock = utils.locks.AsyncLock.request('SingletonClass');
return lock.lockAsync(() => {
if (!LazySingletonClass.instance) {
LazySingletonClass.instance = new LazySingletonClass()
}
return LazySingletonClass.instance;
}, ArkTSUtils.locks.AsyncLockMode.EXCLUSIVE)
}
public setNum(num : number) {
this.num = num;
}
public getNum() : number {
return this.num;
}
}
最终输出如下,由于是多线程操作,每次线程输出顺序不尽相同,但是不影响最终数据为15。(如果increaseNumber没有加锁,结果为5)
以下是不加锁时的错误输出,错误日志每次不同,这里只是截取一次进行说明,可见在increase5和increase7线程中获取到,增长前初始值都是0导致最终输出结果错误。同时由于写线程未加锁,读线程会读取到计算过程中的脏数据,不符合预期;