【鸿蒙实战开发】ArkTS多线性的多线程系列(三):基于单例实现跨线程缓存

单例的使用场景很多,如可以通过单例实现应用缓存,这样多个线程统一对一块内存进行读写数据,既保障了数据的唯一性,又提高了业务处理性能。又比如使用单例实现应用的全局配置管理,保障全局有且仅有一个配置管理对象。

单例模式简介

单例是设计模式使用最为普遍的模式之一。它是一种对象创建模式,用于产生一个对象的具体实例,它可以确保系统中(单进程),一个类只产生一个实例。它的优势在于:

  1. 对于频繁使用的对象,可以省略new操作花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;

  2. 由于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导致最终输出结果错误。同时由于写线程未加锁,读线程会读取到计算过程中的脏数据,不符合预期;

鸿蒙全栈开发全新学习指南

为了积极培养鸿蒙生态人才,让大家都能学习到鸿蒙开发最新的技术,针对一些在职人员、0基础小白、应届生/计算机专业、鸿蒙爱好者等人群,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线【包含了大厂APP实战项目开发】

本路线共分为四个阶段

第一阶段:鸿蒙初中级开发必备技能

在这里插入图片描述

第二阶段:鸿蒙南北双向高工技能基础:gitee.com/MNxiaona/733GH

第三阶段:应用开发中高级就业技术

第四阶段:全网首发-工业级南向设备开发就业技术:gitee.com/MNxiaona/733GH

《鸿蒙 (Harmony OS)开发学习手册》(共计892页)

如何快速入门?

1.基本概念
2.构建第一个ArkTS应用
3.……

开发基础知识:gitee.com/MNxiaona/733GH

1.应用基础知识
2.配置文件
3.应用数据管理
4.应用安全管理
5.应用隐私保护
6.三方应用调用管控机制
7.资源分类与访问
8.学习ArkTS语言
9.……

基于ArkTS 开发

1.Ability开发
2.UI开发
3.公共事件与通知
4.窗口管理
5.媒体
6.安全
7.网络与链接
8.电话服务
9.数据管理
10.后台任务(Background Task)管理
11.设备管理
12.设备使用信息统计
13.DFX
14.国际化开发
15.折叠屏系列
16.……

鸿蒙开发面试真题(含参考答案):gitee.com/MNxiaona/733GH

鸿蒙入门教学视频:

美团APP实战开发教学:gitee.com/MNxiaona/733GH

写在最后

  • 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  • 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
  • 想要获取更多完整鸿蒙最新学习资源,请移步前往小编:gitee.com/MNxiaona/733GH

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值