面试必备:Kotlin 线程同步的 N 种方法

Kotlin实战指南十六:Synchronized、Volatile 文章目录如何创建线程 如何创建线程 在Kotlin 中,我们仍然可以使用 java 的语法创建一个线程 Thread(Runnable { }).start() //或者使用 Lambda 表达式 Thread { }.start() 事实上,kotlin 为我们提供了一个简单写法: Kotlin中可以使用thread()方法创建新的线程,指定的语句块将在新线程中运行。语法简单,十分易用。 fun main() { thread { Log.d("ya 阅读详情

面试的时候经常会被问及多线程同步的问题,例如:

“ 现有 Task1、Task2 等多个并行任务,如何等待全部执行完成后,执行 Task3。”

在 Kotlin 中我们有多种实现方式,本文将所有这些方式做了整理,建议收藏。

1. Thread.join
2. Synchronized
3. ReentrantLock
4. BlockingQueue
5. CountDownLatch
6. CyclicBarrier
7. CAS
8. Future
9. CompletableFuture
10. Rxjava
11. Coroutine
12. Flow

我们先定义三个Task,模拟上述场景, Task3 基于 Task1、Task2 返回的结果拼接字符串,每个 Task 通过 sleep 模拟耗时:

val task1: () -> String = {
    sleep(2000)
    "Hello".also { println("task1 finished: $it") }
}

val task2: () -> String = {
    sleep(2000)
    "World".also { println("task2 finished: $it") }
}

val task3: (String, String) -> String = { p1, p2 ->
    sleep(2000)
    "$p1 $p2".also { println("task3 finished: $it") }
}

1. Thread.join()

Kotlin 兼容 Java,Java 的所有线程工具默认都可以使用。其中最简单的线程同步方式就是使用 Thread 的 join() :

@Test
fun test_join() {
    lateinit var s1: String
    lateinit var s2: String

    val t1 = Thread { s1 = task1() }
    val t2 = Thread { s2 = task2() }
    t1.start()
    t2.start()

    t1.join()
    t2.join()

    task3(s1, s2)

}

2. Synchronized

使用 synchronized 锁进行同步

    @Test
    fun test_synchrnoized() {
        lateinit var s1: String
        lateinit var s2: String

        Thread {
            synchronized(Unit) {
                s1 = task1()
            }
        }.start()
        s2 = task2()

        synchronized(Unit) {
            task3(s1, s2)
        }

    }

但是如果超过三个任务,使用 synchrnoized 这种写法就比较别扭了,为了同步多个并行任务的结果需要声明n个锁,并嵌套n个 synchronized。

3. ReentrantLock

ReentrantLock 是 JUC 提供的线程锁,可以替换 synchronized 的使用

    @Test
    fun test_ReentrantLock() {

        lateinit var s1: String
        lateinit var s2: String

        val lock = ReentrantLock()
        Thread {
            lock.lock()
            s1 = task1()
            lock.unlock()
        }.start()
        s2 = task2()

        lock.lock()
        task3(s1, s2)
        lock.unlock()

    }

ReentrantLock 的好处是,当有多个并行任务时是不会出现嵌套 synchrnoized 的问题,但仍然需要创建多个 lock 管理不同的任务,

4. BlockingQueue

阻塞队列内部也是通过 Lock 实现的,所以也可以达到同步锁的效果

    @Test
    fun test_blockingQueue() {

        lateinit var s1: String
        lateinit var s2: String

        val queue = SynchronousQueue<Unit>()

        Thread {
            s1 = task1()
            queue.put(Unit)
        }.start()

        s2 = task2()

        queue.take()
        task3(s1, s2)
    }

当然,阻塞队列更多是使用在生产/消费场景中的同步。

5. CountDownLatch

JUC 中的锁大都基于 AQS 实现的,可以分为独享锁和共享锁。ReentrantLock 就是一种独享锁。相比之下,共享锁更适合本场景。 例如 CountDownLatch,它可以让一个线程一直处于阻塞状态,直到其他线程的执行全部完成:

    @Test
    fun test_countdownlatch() {

        lateinit var s1: String
        lateinit var s2: String
        val cd = CountDownLatch(2)
        Thread() {
            s1 = task1()
            cd.countDown()
        }.start()

        Thread() {
            s2 = task2()
            cd.countDown()
        }.start()

        cd.await()
        task3(s1, s2)
    }

共享锁的好处是不必为了每个任务都创建单独的锁,即使再多并行任务写起来也很轻松

6. CyclicBarrier

CyclicBarrier 是 JUC 提供的另一种共享锁机制,它可以让一组线程到达一个同步点后再一起继续运行,其中任意一个线程未达到同步点,其他已到达的线程均会被阻塞。

与 CountDownLatch 的区别在于 CountDownLatch 是一次性的,而 CyclicBarrier 可以被重置后重复使用,这也正是 Cyclic 的命名由来,可以循环使用

    @Test
    fun test_CyclicBarrier() {

        lateinit var s1: String
        lateinit var s2: String
        val cb = CyclicBarrier(3)

        Thread {
            s1 = task1()
            cb.await()
        }.start()

        Thread() {
            s2 = task1()
            cb.await()
        }.start()

        cb.await()
        task3(s1, s2)

    }

7. CAS

AQS 内部通过自旋锁实现同步,自旋锁的本质是利用 CompareAndSwap 避免线程阻塞的开销。 因此,我们可以使用基于 CAS 的原子类计数,达到实现无锁操作的目的。

    @Test
    fun test_cas() {

        lateinit var s1: String
        lateinit var s2: String

        val cas = AtomicInteger(2)

        Thread {
            s1 = task1()
            cas.getAndDecrement()
        }.start()

        Thread {
            s2 = task2()
            cas.getAndDecrement()
        }.start()

        while (cas.get() != 0) {}

        task3(s1, s2)

    }

while 循环空转看起来有些浪费资源,但是自旋锁的本质就是这样,所以 CAS 仅仅适用于一些cpu密集型的短任务同步。

volatile

看到 CAS 的无锁实现,也许很多人会想到 volatile, 是否也能实现无锁的线程安全?

    @Test
    fun test_Volatile() {
        lateinit var s1: String
        lateinit var s2: String

        Thread {
            s1 = task1()
            cnt--
        }.start()

        Thread {
            s2 = task2()
            cnt--
        }.start()

        while (cnt != 0) {
        }

        task3(s1, s2)

    }

注意,这种写法是错误的 volatile 能保证可见性,但是不能保证原子性,cnt-- 并非线程安全,需要加锁操作

8. Future

上面无论有锁操作还是无锁操作,都需要定义两个变量s1、s2记录结果非常不方便。 Java 1.5 开始,提供了 Callable 和 Future ,可以在任务执行结束时返回结果。

@Test
fun test_future() {

    val future1 = FutureTask(Callable(task1))
    val future2 = FutureTask(Callable(task2))

    Executors.newCachedThreadPool().execute(future1)
    Executors.newCachedThreadPool().execute(future2)

    task3(future1.get(), future2.get())

}

通过 future.get(),可以同步等待结果返回,写起来非常方便

9. CompletableFuture

future.get() 虽然方便,但是会阻塞线程。 Java 8 中引入了 CompletableFuture ,他实现了 Future 接口的同时实现了 CompletionStage 接口。 CompletableFuture 可以针对多个 CompletionStage 进行逻辑组合、实现复杂的异步编程。 这些逻辑组合的方法以回调的形式避免了线程阻塞:

@Test
fun test_CompletableFuture() {
    CompletableFuture.supplyAsync(task1)
        .thenCombine(CompletableFuture.supplyAsync(task2)) { p1, p2 ->
             task3(p1, p2)
        }.join()
}

10. RxJava

RxJava 提供的各种操作符以及线程切换能力同样可以帮助我们实现需求: zip 操作符可以组合两个 Observable 的结果;subscribeOn 用来启动异步任务

@Test
fun test_Rxjava() {

    Observable.zip(
        Observable.fromCallable(Callable(task1))
            .subscribeOn(Schedulers.newThread()),
        Observable.fromCallable(Callable(task2))
            .subscribeOn(Schedulers.newThread()),
        BiFunction(task3)
    ).test().awaitTerminalEvent()

}

11. Coroutine

前面讲了那么多,其实都是 Java 的工具。 Coroutine 终于算得上是 Kotlin 特有的工具了:

@Test
fun test_coroutine() {

    runBlocking {
        val c1 = async(Dispatchers.IO) {
            task1()
        }

        val c2 = async(Dispatchers.IO) {
            task2()
        }

        task3(c1.await(), c2.await())
    }
}

写起来特别舒服,可以说是集前面各类工具的优点于一身。

12. Flow

Flow 就是 Coroutine 版的 RxJava,具备很多 RxJava 的操作符,例如 zip:

@Test
fun test_flow() {

    val flow1 = flow<String> { emit(task1()) }
    val flow2 = flow<String> { emit(task2()) }

    runBlocking {
         flow1.zip(flow2) { t1, t2 ->
             task3(t1, t2)
        }.flowOn(Dispatchers.IO)
        .collect()

    }

}

flowOn 使得 Task 在异步计算并发射结果。

总结

上面这么多方式,就像茴香豆的“茴”字的四种写法,没必要都掌握。作为结论,在 Kotlin 上最好用的线程同步方案首推协程!

最后

关于Kotlin我有一份完备的学习路线图,对应的知识点也收集了完整的资料。

不仅如此,这一份面试题花了我大量时间进行收集,由于篇幅原因就不全部展示给大家了:

需要更详细面试资料的读者可以点击链接获取:点击链接加入【Android开发交流】

2017年严重拖延着患者欠下的债 扩展基础知识面Android 面试 | 全站式导航http://mp.weixin.qq.com/s/fTfudY1DBYS5JiSkPnbjAg100篇精选干货,感谢你与码个蛋共同成长(含5重福利)http://mp.weixin.qq.com/s/KSR1QFClkrap1ii3eTRSkQ《Android 开发工程师面试指南》 https://www.diycode.cc/wiki/andr 阅读详情

相关推荐

Kotlin中线程同步的几种方法

本文介绍了Kotlin中线程同步的几种常见方法,包括使用synchronized关键字、ReentrantLock和Semaphore。根据具体的需求和场景,我们可以选择适合的线程同步方法来实现线程安全的代码。ReentrantLock是Java提供的另一种线程同步机制,它也可以在Kotlin中使用。在Kotlin中,线程同步是一种重要的技术,用于确保多个线程能够正确地协同工作,避免出现竞态条件和数据不一致等问题。它可以用来修饰方法或代码块,确保同一时间只有一个线程可以进入被修饰的代码区域。

2301_79326588的博客 1398

Kotlin多线程

如果在操作对象之前,线程先声明:“这个对象是我的”,当另一个线程也想操作这个对象时,发现已经有人声明过了,那么它就等待,直到那个发布声明的线程又发了一个“这个对象不是我的了”的声明。当然,这两个声明其实起到了一个锁的作用,当声明“这个对象是我的”时,对象就被上了锁,当声明“这个对象不是我的了”时,对象的锁就被解开了。因为main线程创建了th线程,th线程又创建了th2线程。th线程调用join()方法等待th2线程时,main线程中断了th线程,因此th线程中的join()方法停止等待,执行完成。

wozuishuai_的博客 2900

浅析 Kotlin 中的 synchronized

首先,在 Java 中 synchronized 是一个关键字,在Kotlin 中是一个函数。这个函数如下: /* * Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors. * Use of this source code is governed by the Apache 2.0...

AI天才研究院 1万+

Kotlin 多线程(1)

多线程

伯特立的专栏 4507

FactoryTool量产神器实战:5分钟搞定多块RK3568板卡并行烧录

本文详细介绍了使用瑞芯微FactoryTool对多块RK3568板卡进行高效并行烧录的实战指南。文章从硬件连接、驱动配置、软件操作到排错技巧,提供了一套完整的量产解决方案,重点阐述了如何利用该工具在5分钟内完成多设备固件烧录,并确保eMMC存储的写入稳定性与效率,大幅提升生产线吞吐量。

b0c1d2的博客 990

Windows11安装UE4+Airsim

Windows11安装UE4+Airsim

Legendyyy的博客 3743

Android快速转战Kotlin教程,面试必备

前言 我们移动开发程序员应该首先明白一个要点,能够学习的东西可以区分为『知识』和『技能』。 知识,就是你知道就知道、不知道就不知道的东西,比如『计算机系统中一个字节是包含8个bit』,你知道了之后就算掌握了。 技能,是需要练习的东西,不是知道不知道的区别,只有熟练还是不熟练的区分,比如『写代码要用有意义的命名规则』,你知道了这一点,不代表你掌握了这个写代码命名的技巧,你需要去写代码练习,但是练习还不够,你需要反复练习,还需要别人给你反馈,你才能练习得更好。 是的,知乎上有些内容全都是『知识』(有的还只是伪知

m0_56252652的博客 172

LMX2595实战:手把手教你配置JESD204B时钟与SYSREF(含相位同步避坑指南)

本文详细介绍了LMX2595在JESD204B时钟系统中的配置与优化方法,包括时钟系统架构设计、相位同步配置、SYSREF生成技巧以及PCB布局规范。通过实战案例和避坑指南,帮助工程师充分发挥LMX2595的超低相位噪声和抖动性能,确保高速数据转换器系统的稳定性和同步精度。

weixin_29206669的博客 478

2025年Android面试全攻略:Kotlin、Flutter与架构设计

在移动开发领域,JVM内存管理和多线程并发是永恒的技术基础。现代Android开发中,Kotlin协程通过挂起恢复机制实现了更高效的异步处理,其Continuation接口和CPS转换原理值得深入理解。Flutter作为跨平台方案的核心优势在于高性能渲染引擎和灵活的Widget体系,MethodChannel的通信机制支撑了混合开发场景。架构设计层面,MVI模式结合StateFlow实现了单向数据流,而依赖注入技术如Hilt则提升了代码可维护性。这些技术在电商、社交等高频交互App中都有广泛应用,特别是在处

weixin_34378767的博客 403

【原创理论】Stochastic Coupled Dyadic System (SCDS):一个用于两性关系动力学建模的随机耦合系统框架

基于二人爱情耦合系统动力学、稳定性与最优策略的综合性理论的数值模拟与分析

weixin_44695861的博客 1134

解决Windows 11文件右键菜单卡死问题的方法

当你右键点击一个文件或者文件夹时,系统会加载与该文件类型相关的上下文菜单项,如果其中某个菜单项的代码执行时间过长或者出现错误,就会导致整个右键菜单卡死。综上所述,当你在Windows 11中遇到右键菜单卡死的问题时,你可以尝试禁用上下文菜单项、重置文件关联、排除第三方软件干扰、修复系统文件以及优化代码等方法来解决问题。如果你是一个开发者,并且你自己编写了一个上下文菜单项,那么问题可能出在你的代码中。例如,如果你遇到了图片文件的右键菜单卡死问题,你可以找到".jpg"或".png"等相关的子项。

HnrzLinux的博客 7536

太香了,Android面试主题整理合集

在金三银四即将来临之际,给大家整理了一份中高级Android面试题合集, 第一章Java部分 1.设计模式(六大基本原则、项目中常用的设计模式、手写单例等) 你所知道的设计模式有哪些 单例设计模式 工厂设计模式 建造者模式(Builder) 适配器设计模式 装饰模式(Decorator) 观察者模式(Observer) 2.Java语言的特点与OOP思想 3.说下java中的线程创建方式,线程池的工作原理。 4. HashMap ArrayList的构造器 add方法源码分析 get方法源码分析 se

Android578的博客 247

Arcgis中的高级模型构建器技术之图层按某一字段批量转KMZ

本文介绍在ArcGIS模型构建器中创建按属性批量转换KMZ的工具。通过迭代要素选择工具按字段属性分类,结合应用图层符号设置和图层转KML工具,实现将单个图层按指定属性(如规划地类)自动分批次转换为带符号的KMZ文件。关键步骤包括:1)设置迭代要素选择参数;2)应用预设符号系统;3)配置KMZ输出路径和命名规则。该工具可高效处理规划图斑等数据,保持分类符号化效果,输出结果为按属性值命名的多个KMZ文件。

weixin_44671081的博客 471

Java基础核心考点深度解析:Android面试必备与实战应用

Java作为Android开发的基石语言,其核心原理深刻影响着应用性能与稳定性。理解Java内存模型(JMM)和垃圾回收(GC)机制,是解决内存泄漏和优化程序运行效率的关键。在并发编程中,掌握线程池、锁机制及原子类,能有效提升多线程任务的管理能力与数据安全性。这些基础概念直接映射到Android开发场景,例如Handler的内存泄漏防范、RecyclerView数据源的高效管理,以及全局缓存的多线程安全设计。本文聚焦HashMap、ConcurrentHashMap等集合框架的演进与差异,并深入剖析sync

weixin_30861459的博客 347

2025.05.29【Network】多组学分析:网络互作图绘制

本文介绍了使用R语言进行网络图绘制的方法与技巧。重点讲解了三个核心R包的使用:igraph用于网络图数据准备与基础绘制,ggraph基于图形语法实现高级可视化,networkD3则为网络图添加交互功能。文章涵盖了从基本概念、包安装、创建网络图对象,到自定义样式布局、添加交互性等完整流程,并提供了度分布、聚类系数等网络分析指标的计算方法。通过清晰的代码示例,帮助读者掌握生物信息学数据可视化的关键技能,为复杂数据的直观展示和分析提供实用工具。

227

QGis二次开发 —— 1.1、ubuntu24.04下搭建QGis3.40.12二次开发环境(附:全部问题解决方法)

QGis二次开发 —— 1.1、ubuntu24.02下搭建QGis3.40.12二次开发环境(附:全部问题解决方法)

251

【2024版Android面试宝典】,腾讯等大厂Android高级开发面试完全攻略手册!(2)

设个问题涉及的内容很多,下面的版块会展开进行详细讲解。面试流程大厂的部门一般比较多,岗位类型多,职级差异也大,所以大家的面试流程都是不一样的。一般来说,针对高开,大部分的大厂面试都会经历2轮技术面,1轮领导面,1轮HR面,如果面试情况有争议(或者面的特别好的情况)会有一轮加面,总体来讲总共会有4轮面试。以字节为例,基本上所有面试都会在线上进行,一面2轮技术面,2面领导面(也会有一些技术的考察),3面就是HR面谈薪资和定级了。这里图解说一下普适性的面试流程。(PS:面试如打怪升级,大家要先通关前面才能进入后面

2401_84876174的博客 1010

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

kotlin教程!Android跨进程通信导论,吊打面试官系列!

最近跟我的一些读者交流,有一位读者的经历让我记忆深刻: “有一次和大学同学聚会,和几个在BAT的同学聊了聊技术,发现自己在创业公司这几年,完全是吃老本的状态,没有什么机会精进技术,同样是工作了三年,和同学的差距越来越大” 我继续问他,他说真正让他受打击的是这个月的一次面试。 “面的是一个大厂,面试官问了很多问题,一大半的问题我都是一知半解,回答上来问题也是特没底。 大概回顾了这场面试:上来先是问了Java基础知识,自定义view,算法等等,这部分我还算准备的充分。第二部分开始复杂了,主要是底层、N

m0_56146626的博客 177

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

2024年太香了,Android面试主题整理合集(3),2024年最新面试自我介绍模板

跳槽季整理面试题已经成了我多年的习惯!在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。

2401_84545468的博客 655

Android开发者面试如何系统复习?帮你突破瓶颈

我究竟要不要学习Kotlin呢? 要回答这个问题,我们先把时间拨回到2017年5月18日,安卓团队在谷歌 I/O 2017 大会上宣布 Kotlin 成为官方头等支持语言。 彼时谷歌技术大牛 Steve Yegge 发表了一篇关于 Kotlin 的使用体会总结文章:“为什么说 Kotlin 比你们用的那些垃圾语言都好?” 一时间这条爆炸性资讯在引发的热议一直持续到了今天。 时间是检验真理的唯一标准,接下来我们来看看国内外技术专家,使用kotlin开发的感受! Google Android 专家A

m0_56830725的博客 143

【面经分享】5年Android开发的求职之路

前段时间在看机会,本文就是我对求职过程的一个总结。 同时,也作为一个面试官,来说说求职中需要注意的点。

yechaoa 5615

2024年安卓最新当了5年的Android面试官,特此前来总结几点经验,2024年最新工程建设面试问题

Android中的每一个UI控件都是集成自View,然后这些View都具有相同的绘制流程,必须经过measure,layout和draw. view的绘制流程是在Window添加过程中,ViewRootImpl类的setView方法开始的。这也是我比较看重的一点,这里并不是指那些臭名昭彰的脑经急转弯问题,而是通过交流观察,判断一个人表达观点逻辑是否清晰,回答问题是否有章法,这个很难描述,但如果你细心观察,你会发现很容易通过一些简单的交流,就可以看出一一个人是否逻辑清晰。文章里已经展示了部分!

2401_84562377的博客 809

Android面试必考:Java基础核心概念与实战应用深度解析

面向对象编程是软件开发的基石,其核心在于通过类与对象实现代码的封装、继承和多态,从而构建出模块化、可复用的软件结构。这一原理在Android开发中体现得尤为深刻,例如Activity的生命周期管理、事件监听机制都依赖于对象的多态性和JVM的内存管理机制。理解这些底层机制的技术价值在于,它能帮助开发者编写出更健壮、可维护的代码,并从根本上解决内存泄漏、ANR等性能瓶颈问题。在实际应用场景中,无论是优化HashMap性能、正确使用并发工具避免卡顿,还是通过分析JVM内存模型来定位OOM崩溃,扎实的Java基础都

weixin_30631587的博客 334

android开发基础机构,字节跳动厂内部超高质量Flutter+Kotlin笔记

说下Activity生命周期?Activity A 启动另一个Activity B 会调用哪些方法?如果B是透明主题的又或则是个DialogActivity呢?说下onSaveInstanceState()方法的作用?何时会被调用?说下 Activity的四种启动模式、应用场景?了解哪些Activity常用的标记位Flags?说下 Activity跟window,view之间的关系?横竖屏切换的Activity生命周期变化?如何启动其他应用的Activity?Activity的启动过程?

m0_60635609的博客 969
上一篇: 我靠加班把老员工弄走了,昨天老板又给我画饼,一天天浑浑噩噩,Android开发究竟还有前途吗?
下一篇: 记一次Android 面试,包含面试题答案
懂你的大海
博客等级 码龄6年 9049粉丝 1381原创
评论 4
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值