一文认知并发安全的几种解决方案与性能对比

news2025/1/23 3:48:13

Kotlin协程基本套餐:

  • 协程的基本使用

  • 协程的上下文理解

  • 协程的作用域管理

  • 协程的常见进阶使用

之前的系列文章我们讲的是一些 Kotlin 协程的基本概念和一些实用与常用的技巧与方法。其实明白之后,基本的使用是没有问题了。

那么今天我想探讨一下,没有那么常用,但是也很重要的一个点,协程的并发与安全。

为什么并发这么重要,还放在这一篇单独讲呢?使用我们客户端其实并发的场景并不多,除了一些指定的特殊场景,我们一般并发也只是同时执行一些任务而已,很少会在并发的场景中去修改同一个值,当然如果真的有这种操作,我这不是来了吗?

下面我们一起来看看并发中操作同一个值的时候我们该如何保证数据安全问题。

关于如何在协程并发我们在之前的文章已经讲解过,创建协程就是并发,但是之前的文章我们只是大致的说了不推荐使用同步锁,我们使用的是 mutex 互斥锁来做的演示。

那么我们这一期一开始我们就详细的讲一下线程锁/同步锁与协程的互斥锁的一些用法,它们之间的区别。

1.1 异步并发的实现

那么我们先来一段代码,实现一个并发,由于数据量很大,这些计算我们在子线程实现,我们就以并发+异步来举例了:

        runBlocking {
            var count = 0val job1 = CoroutineScope(Dispatchers.IO).launch {
                repeat(99999) {
                    count++
                }
            }
            val job2 = CoroutineScope(Dispatchers.IO).launch {
                repeat(99999) {
                    count--
                }
            }
            job1.join()
            job2.join()

            //等待Job1 Job2执行完毕打印结果
            YYLogUtils.w("count: $count")
        }
复制代码

打印结果: 无锁耗时为165ms左右

1.2 synchronized 关键字

可以看到结果是不会为0的,可能有同学很快的反应过来了,需要加锁,线程同步锁我们最先想到的是 synchronized 关键字,我们看看如何使用:

      runBlocking {
                var count = 0val lock = "lock"// 需要保证锁的是同一个对象val job1 = CoroutineScope(Dispatchers.IO).launch {
                    repeat(99999) {
                        synchronized(lock) {
                            count++
                        }
                    }
                }
                val job2 = CoroutineScope(Dispatchers.IO).launch {
                    repeat(99999) {
                        synchronized(lock) {
                            count--
                        }
                    }
                }
                job1.join()
                job2.join()

                //等待Job1 Job2执行完毕打印结果
                YYLogUtils.w("count: $count")
            }

        }

     YYLogUtils.w("count:执行耗时:$time")

复制代码

打印结果:

可以看到确实加了锁之后就能保证运行的结果为0,但是耗时会增加。这也都是正常的。

那么我们想优化一下使用 launch 替代 runBlocking 看看效果,能起到优化的作用吗?

 launch {
            val start = System.currentTimeMillis()
            var count = 0val lock = "lock"val job1 = async(Dispatchers.IO) {
                repeat(99999) {
                    synchronized(lock) {
                        count++
                    }

                }
            }
            val job2 = async(Dispatchers.IO) {
                repeat(99999) {
                    synchronized(lock) {
                        count--
                    }
                }
            }

            job1.join()
            job2.join()


            //等待Job1 Job2执行完毕打印结果
            YYLogUtils.w("count: $count")
            YYLogUtils.w("count:执行耗时:${System.currentTimeMillis() - start}")
        }
复制代码

打印结果:

可以看到这是线程的同步操作,我们改协程的启动方式,是否阻塞协程这些东西是没有效果的

1.3 自行实现 ReentrantLock 可重入锁

那么除了 synchronized 关键字,我们还有没有其他的办法加锁,有的,我们可以自行实现锁的逻辑,例如我们可以使用 ReentrantLock 可重入锁来实现

val time = measureTimeMillis {

            runBlocking {
                var count = 0val lock = ReentrantLock()

                val job1 = CoroutineScope(Dispatchers.IO).launch {
                    repeat(99999) {
                        lock.lock()
                        count++
                        lock.unlock()
                    }
                }
                val job2 = CoroutineScope(Dispatchers.IO).launch {
                    repeat(99999) {
                        lock.lock()
                        count--
                        lock.unlock()
                    }
                }
                job1.join()
                job2.join()

                //等待Job1 Job2执行完毕打印结果
                YYLogUtils.w("count: $count")
            }

        }

        YYLogUtils.w("count:执行耗时:$time")
复制代码

打印的结果:

这种锁和读写锁有点类似,我们需要手动的加锁,释放锁。那么首先可以看到结果确实是我们预期的,但是这个效率太慢了,是不如 synchronized 关键字的。

那么这种线程同步锁的操作,是否可以通过修改协程的阻塞启动方式来优化呢?试试

 launch {
            val start = System.currentTimeMillis()
            var count = 0val lock = ReentrantLock()

            val job1 = async(Dispatchers.IO) {
                repeat(99999) {
                    lock.lock()
                    count++
                    lock.unlock()

                }
            }
            val job2 = async(Dispatchers.IO) {
                repeat(99999) {
                    lock.lock()
                    count--
                    lock.unlock()
                }
            }

            job1.join()
            job2.join()


            //等待Job1 Job2执行完毕打印结果
            YYLogUtils.w("count: $count")
            YYLogUtils.w("count:执行耗时:${System.currentTimeMillis() - start}")
        }
复制代码

打印结果:

可以看到线程的锁的方式,如果修改协程的启动方式是没有优化效果的

1.4 mutex的实现

之前的方式都是我们线程同步的概念,接下来我们可以看看协程专用同步工具 mutex 如何使用:

val time = measureTimeMillis {

            runBlocking {
                var count = 0val mutex = Mutex()

                val job1 = CoroutineScope(Dispatchers.IO).launch {
                    repeat(99999) {
                        mutex.withLock {
                            count++
                        }

                    }
                }
                val job2 = CoroutineScope(Dispatchers.IO).launch {
                    repeat(99999) {
                        mutex.withLock {
                            count--
                        }
                    }
                }
                job1.join()
                job2.join()

                //等待Job1 Job2执行完毕打印结果
                YYLogUtils.w("count: $count")
            }

        }

        YYLogUtils.w("count:执行耗时:$time")
复制代码

打印的结果:

可以看到结果是我们预期的,达到了我们想要的效果,mutex.withLock 内部也是实现了加锁,释放锁的逻辑。但是这个耗时还不如 ReentrantLock 锁呢...

之前的线程同步方式我们修改启动模式貌似并不能起到优化作用,那么协程的锁呢?

我们已经使用 mutex 限制协程了,那么我们不需 runBlocking 来阻塞协程了,我们试试 launch 的方式,看看是否有所缓解。

     launch {
            val start = System.currentTimeMillis()
            var count = 0val mutex = Mutex()


            val job1 = async {
                repeat(99999) {
                    mutex.withLock {
                        count++
                    }

                }
            }
            val job2 = async {
                repeat(99999) {
                    mutex.withLock {
                        count--
                    }
                }
            }

            job1.await()
            job2.await()


            //等待Job1 Job2执行完毕打印结果
            YYLogUtils.w("count: $count")
            YYLogUtils.w("count:执行耗时:${System.currentTimeMillis() - start}")
        }
复制代码

改善之后的结果:

协程的锁的方式,我们可以通过修改启动方式来优化效率,优化效果很明显

1.5 Semaphore 指定通道数量

Semaphore是协程中的信号量 ,我们指定通行的数量为1,那么就可以保证并发的数量为1,这样异曲同工达到锁的效果。

val time = measureTimeMillis {

            runBlocking {
                var count = 0val semaphore = Semaphore(1)

                val job1 = CoroutineScope(Dispatchers.IO).launch {
                    repeat(99999) {
                        semaphore.withPermit {
                            count++
                        }

                    }
                }
                val job2 = CoroutineScope(Dispatchers.IO).launch {
                    repeat(99999) {
                        semaphore.withPermit {
                            count--
                        }
                    }
                }

                job1.join()
                job2.join()

                //等待Job1 Job2执行完毕打印结果
                YYLogUtils.w("count: $count")
            }

        }

        YYLogUtils.w("count:执行耗时:$time")
复制代码

打印如下:

结果如预期,但是这也太慢了,我们已经使用 semaphore 限制运行协程的数量,那么我们不需 runBlocking 来阻塞协程了,我们试试 launch 的方式

      launch {
            val start = System.currentTimeMillis()
            var count = 0val semaphore = Semaphore(1)


            val job1 = async {
                repeat(99999) {
                    semaphore.withPermit {
                        count++
                    }

                }
            }
            val job2 = async {
                repeat(99999) {
                    semaphore.withPermit {
                        count--
                    }
                }
            }

            job1.await()
            job2.await()


            //等待Job1 Job2执行完毕打印结果
            YYLogUtils.w("count: $count")
            YYLogUtils.w("count:执行耗时:${System.currentTimeMillis() - start}")
        }
复制代码

打印结果有所优化:

协程的锁的方式,我们可以通过修改启动方式来优化效率,优化效果很明显

1.6 AtomicInteger 保证原子操作

除了线程锁和协程锁的方法,我们还能使用 AtomicInteger 包装count,实现原子操作,从而间接的实现锁的效果。

       launch {
            val start = System.currentTimeMillis()
            val count = AtomicInteger(0)

            val job1 = async (Dispatchers.IO) {
                repeat(99999) {
                    count.incrementAndGet()
                }
            }
            val job2 = async (Dispatchers.IO) {
                repeat(99999) {
                    count.decrementAndGet()
                }
            }

            job1.join()
            job2.join()


            //等待Job1 Job2执行完毕打印结果
            YYLogUtils.w("count: $count")
            YYLogUtils.w("count:执行耗时:${System.currentTimeMillis() - start}")
        }
复制代码

效果也是相当不错的:

1.7 单线程池实现同步效果

上面的一些方法主流都是给线程或协程加同步锁,就是当有程序在执行的时候,你等等,等你前面的程序执行完成之后你再执行,只是效率和开销有所不同而已。

那么我们能不能换一个思路,我保证协程运行在一个单独的线程池不就行了吗?之前的协程基础文章中我们讲到过,线程池的扩展方法可以转为一个协程上下文对象,我们初始化一个 newSingleThread 的线程池不就行了吗?一样的可以到达锁的效果。

试试看行不行:

val singleDispatcher = Executors.newSingleThreadExecutor {
            Thread(it, "SingleThread").apply { isDaemon = true }
        }.asCoroutineDispatcher()


        lifecycleScope.launch {
            val start = System.currentTimeMillis()
            var count = 0val job1 = launch(singleDispatcher) {
                repeat(99999) {
                    count++
                }
            }

            val job2 = launch(singleDispatcher) {
                repeat(99999) {
                    count--
                }
            }

            job1.join()
            job2.join()

            //等待Job1 Job2执行完毕打印结果
            YYLogUtils.w("count: $count")
            YYLogUtils.w("count:执行耗时:${System.currentTimeMillis() - start}")
        }
复制代码

打印日志:

确实能保证结果是我们想要的,并且效率还不低呢,不错不错。注意这里原理是基于单线程实现的,所以修改协程的启动方式 launch runBlocking 效果是一致的。

1.8 actor并发同步模型

actor 是创建协程的一种,但是是特殊的协程,他是继承Channel,关于通道或者叫协程的通信的理解,可以参考我的这一篇文章 Kotlin协程-协程之间的通信与广播。

我们看看 actor 的部分源码:

publicfun<E> CoroutineScope.actor(
    context: CoroutineContext = EmptyCoroutineContext,
    capacity: Int = 0, // todo: Maybe Channel.DEFAULT here?
    start: CoroutineStart = CoroutineStart.DEFAULT,
    onCompletion: CompletionHandler? = null,
    block: suspendActorScope<E>.() -> Unit
): SendChannel<E> {
复制代码

这里我们简单的理解为并发的2个协程与actor的协程通信,actor是一个特殊的协程,保证了单一的原则。从而间接达到锁的效果

这里直接上代码:

      runBlocking {

            val start = System.currentTimeMillis()
            var count = 0suspendfunaddActor() = actor<Int> {

                for (msg in channel) {
                    when (msg) {
                        0 -> count++
                        1 -> count--
                    }
                }
            }

            val actor = addActor()

            val job1 = CoroutineScope(Dispatchers.IO).launch {
                repeat(99999) {
                    actor.send(0)//加
                }
            }

            val job2 = CoroutineScope(Dispatchers.IO).launch {
                repeat(99999) {
                    actor.send(1)//减
                }
            }

            job1.join()
            job2.join()

            val deferred = CompletableDeferred<Int>()
            deferred.complete(count)
            val result = deferred.await()

            actor.close()

            //等待Job1 Job2执行完毕打印结果
            YYLogUtils.w("count: $result")
            YYLogUtils.w("count:执行耗时:${System.currentTimeMillis() - start}")
        }
复制代码

打印结果为:

简直不能接受,我们修改构造参数加入缓存机制 Channel.BUFFERED

suspendfunaddActor() = actor<Int>(capacity = Channel.BUFFERED) {

    for (msg in channel) {
        when (msg) {
            0 -> count++
            1 -> count--
        }
    }
}
复制代码

打印的结果:加入容量为64的缓存,效率提升10倍

我们换一种容器为Max的缓存,看看效果:

suspendfunaddActor() = actor<Int>(capacity = Channel.UNLIMITED) {

    for (msg in channel) {
        when (msg) {
            0 -> count++
            1 -> count--
        }
    }
}
复制代码

打印效果:效率提升就没有那么大了,遇到了性能瓶颈

就算如此,他的效率优化之后也满足不了我们的使用,对排名并没有的影响,它还是最耗时的。可能它的作用只是用于 Channel 中通信的场景下保证安全吧,我们这么直接暴力的使用它老保证普通的并发安全并不是很合适。

总结

优化完成之后我们可以看到并发锁的效率 synchronized(线程同步锁) > AtomicInteger(原子操作) >SingleThreadExecutor(单线程) > ReentrantLock(可重入锁) > Semaphore(协程信号量限制) > Mutex(协程互斥锁)> actor通信并发同步模型

有同学可能会说,都协程了还用 synchronized ?喽不喽啊。。。好吧,其实谷歌自己都用,Flow是运行在协程上的,Flow的源码中几乎都是用 synchronized 来加锁的。我就不一一举例了。

Ok,协程的并发与并发过程中的数据一致性解决方案就讲到这里了。

相信大家理解之后在开发的过程中更能得心应手,关于并发与安全全网很少有比较性能相关的文章,如果有错漏或者使用不当的地方,还望大家指正。

如果大家看的过程有不明白的我更推荐你从系列的第一篇开始看,内部概念与难度是一步一步层层递进的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/397176.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

用gin写简单的crud后端API接口

提要使用gin框架(go的web框架)来创建简单的几个crud接口)使用技术: gin sqlite3 sqlx创建初始工程新建文件夹,创建三个子文件夹分别初始化工程 go mod如果没有.go文件,执行go mod tidy可能报错(warning: "all" matched no packages), 可以先不弄,只初始化模块就行(…

GreenPlum小结

什么是GreenPlum&#xff1f;GreenPlum是业界最快最高性价比的关系型分布式数据库,它在开源的PostgreSQL的基础上采用MPP架构&#xff08;Massive Parallel Processing&#xff0c;海量并行处理&#xff09;,具有强大的大规模数据分析任务处理能力。GreenPlum作为大数据融合存储…

【UE4 RTS游戏】03-摄像机运动_旋转视角

效果可以通过WASD控制“CameraPawn”的移动&#xff1b;通过鼠标中键旋转视角&#xff1b;通过alt鼠标中键将视角回归默认值&#xff1b;通过shift加速移动。步骤打开“CameraPawnController”&#xff0c;给如下节点添加注释&#xff0c;命名为“MovementX”接下来开始开始编辑…

JDK解压安装及idea开发工具配置

1. 安装JDK 1.1 下载安装包 下载安装包&#xff0c;直接解压&#xff0c;注意&#xff0c;解压的路径不要有中文 1.2 配置环境变量 右键点击我的电脑&#xff0c;选择属性 选择高级系统设置 选择环境变量 选择新建 在变量名中输入JAVA_HOME&#xff0c;变量值就是1.1中压缩包…

Windows环境下实现设计模式——访问者模式(JAVA版)

我是荔园微风&#xff0c;作为一名在IT界整整25年的老兵&#xff0c;今天总结一下Windows环境下如何编程实现访问者模式&#xff08;设计模式&#xff09;。不知道大家有没有这样的感觉&#xff0c;看了一大堆编程和设计模式的书&#xff0c;却还是很难理解设计模式&#xff0c…

【C++】类和对象(收尾)

文章目录成员变量初始化问题初始化列表explicit关键字static成员特性&#xff1a;友元友元函数友元类内部类特性匿名对象成员变量初始化问题 在创建对象时&#xff0c;编译器通过调用构造函数&#xff0c;给了对象中各个成员变量一个合适的初始值。但是这并不能够称为对对象中成…

简单了解蓄电池在直流系统中的使用现状!

一般情况下&#xff0c;由市电通过直流配电屏为变电站的直流系统提供工作电源&#xff0c;包括对蓄电池组进行饱和和充电使蓄电池处于备用状态&#xff0c;当交流失电或系统需要进行大电流供电时&#xff0c;蓄电池需要迅速切入&#xff0c;向事故负荷、自动装置、保护装置以及…

本地套接字

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起探讨和分享Linux C/C/Python/Shell编程、机器人技术、机器学习、机器视觉、嵌入式AI相关领域的知识和技术。 本地套接字专栏&#xff1a;《Linux从小白到大神》《网络编程》 本地套接字通信需要一个文件&#xff…

tensorflow【import transformers 报错】

目录 一、安装 安装好了tensorflow,但是import时候报错&#xff1a; import transformers 报错 一、安装 &#xff08;1&#xff09;创建环境&#xff1a; conda create -n [name] python3.3-3.7 &#xff08;2&#xff09;激活环境&#xff1a; conda activate [name] …

Python中赋值、引用、深浅拷贝的区别和联系

文章目录一、对象的唯一id二、赋值三、可变对象和不可变对象四、函数的参数传递五、深拷贝和浅拷贝六、举个栗子6.1 不可变对象的拷贝6.2 可变对象的拷贝6.3 可变对象改变外层元素6.4 可变对象改变内层元素七、总结一、对象的唯一id python中的所有对象都有自己的唯一id&#…

典型回溯题目 - 全排列(一、二)

典型回溯题目 - 全排列&#xff08;一、二&#xff09; 46. 全排列 题目链接&#xff1a;46. 全排列状 题目大意&#xff1a; 给定一个不含重复数字的数组 nums &#xff0c;返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。 注意&#xff1a;&#xff08;1&#xf…

Linux命令·which·whereis·locate·find

我们经常在linux要查找某个文件&#xff0c;但不知道放在哪里了&#xff0c;可以使用下面的一些命令来搜索&#xff1a; which 查看可执行文件的位置。whereis 查看文件的位置。 locate 配合数据库查看文件位置。find 实际搜寻硬盘查询文件名称。whichwhich命令的作用是&#x…

DJ1-1 操作系统引论

目录 一、操作系统的概念 二、操作系统的目标 三、操作系统的作用 一、操作系统的概念 定义一 操作系统是一组控制和管理计算机软硬件资源、合理地对各类作业进行调度以及方便用户使用的程序集合。 定义二 操作系统是位于硬件层&#xff08;HAL&#xff09;之上&#xff…

SQL 基础函数,通配符,BETWEEN ,用法复习

使用 SQL _ 通配符 下面的 SQL 语句选取 name 以一个任意字符开始&#xff0c;然后是 “oogle” 的所有客户&#xff1a; SELECT * FROM Websites WHERE name LIKE _oogle;下面的 SQL 语句选取 name 以 “G” 开始&#xff0c;然后是一个任意字符&#xff0c;然后是 “o”&am…

看完这篇我不信你不会二叉树的层序遍历【C语言】

目录 实现思路 代码实现 之前介绍了二叉树的前、中、后序三种遍历&#xff0c;采用的是递归的方式。今天我们来学习另外一种遍历方式——层序遍历。层序遍历不容小觑&#xff0c;虽然实现方法并不难&#xff0c;但是它所采取的思路是很值得学习的&#xff0c;与前三者不同&am…

学习笔记-架构的演进之容器的封装-3月day06

文章目录前言封装应用的Dockerwhy Docker not LXC?附前言 当文件系统、访问、资源都可以被隔离后&#xff0c;容器就已经具备它降生所需要的全部前置支撑条件了。为了降低普通用户综合使用 namespaces、cgroups 这些低级特性的门槛&#xff0c;2008 年 Linux Kernel 2.6.24 内…

Java中的final和权限修饰符

目录 final 常量 细节&#xff1a; 权限修饰符 Java权限修饰符用于控制类、方法、变量的访问范围。Java中有四种权限修饰符&#xff1a; 权限修饰符的使用场景&#xff1a; final 方法 表明该方法是最终方法&#xff0c;不能被重写。类 表明该类是最终类&#xff0c;不能被继…

Jetpack太香了,让开发效率提升了不少

作者&#xff1a;Jingle_zhang 第三方App使用Jetpack等开源框架非常流行&#xff0c;在Gradle文件简单指定即可。然而ROM内置的系统App在源码环境下进行开发&#xff0c;与第三方App脱节严重&#xff0c;采用开源框架的情况并不常见。但如果系统App也集成了Jetpack或第三方框架…

【UE4 RTS游戏】04-摄像机运动_鼠标移动到视口边缘时移动Pawn

效果可以看到当鼠标移动到视口边缘时&#xff0c;Pawn就会向这个方向移动。步骤打开项目设置&#xff0c;添加两个操作映射打开“CameraPawnController”&#xff0c;在事件图表中添加两个浮点型变量&#xff0c;一个为公有一个为私有。分别命名为“ZoomSensitivity”、“MaxAr…

【Linux】帮助文档查看方法

目录1 Linux帮助文档查看方法1.1 man1.2 内建命令(help)1 Linux帮助文档查看方法 1.1 man man 是 Linux 提供的一个手册&#xff0c;包含了绝大部分的命令、函数使用说明。 该手册分成很多章节&#xff08;section&#xff09;&#xff0c;使用 man 时可以指定不同的章节来浏…