Kotlin多线程

news2025/1/16 14:03:05

目录

线程的使用

线程的创建

例一:创建线程并输出Hello World

Thread对象的用法

start()

join()

interrupt()

线程安全

原子性

可见性

有序性

线程锁

ReentrantLock

ReadWriteLock


线程的使用

Java虚拟机中的多线程可以1:1映射至CPU中,即一个CPU线程跑一个任务,这叫并行,也可以N:1地运行,即一个CPU线程交替跑多个任务,看起来是同时地。这两种方法都叫并发

线程的创建

kotlin中,可以通过kotlin.concurrent包下的thread函数创建一个线程:

fun thread(
    start: Boolean = true,
    isDaemon: Boolean = false,
    contextClassLoader: ClassLoader? = null,
    name: String? = null,
    priority: Int = -1,
    block: () -> Unit
): Thread

该函数接收6个参数,必须定义block参数,因为它是线程的执行函数:

  • start: 如果为真,则立即执行
  • isDaemon: 如果为真,则会创建守护线程。当所有正在运行的线程都是守护线程时,Java虚拟机将自动退出
  • contextClassLoader: 线程中所使用的类加载器,又叫上下文类加载器。如果不指定类加载器,则会使用系统的类加载器
  • name: 线程的名字
  • priority: 线程的优先级。只有该参数大于0时才有效。线程的优先级在1-10之间,默认为5. 线程的优先级的最大值、最小值、默认值被分别定义在java.long包下Thread类的静态变量MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY内
  • block: 一个回调函数,无参数,无返回值,线程运行调用此方法

该函数返回一个java.long包下的Thread对象,表示创建的线程。

因为该函数的前五个参数都有默认值,因此可以使用kotlin的语法糖,简化thread的用法:

thread { 
    println("Hello World")
}

例一:创建线程并输出Hello World

import kotlin.concurrent.thread

fun main() {

    thread {
        println("Hello World")
    }

}

这就是这个例子的全部代码了,是不是非常简单?

在main方法里,创建了一个线程,线程执行时打印Hello World.

Thread对象的用法

我们提到,thread函数会返回一个Thread对象,那么,如何使用这个Thread对象呢?

首先,Thread类是用Java写的,所以它的函数原型是Java形式的

start()

Thread对象中有start()方法,表示执行线程:

public void start()

如果我们在thread方法中设置start参数为false,那么我们可以通过调用start()方法执行线程:

import kotlin.concurrent.thread

fun main() {

    val th = thread(start = false) {
        println("Hello World")
    }
    
    println("准备启动线程")
    th.start()

}

执行结果:

准备启动线程
Hello World
join()

join()方法等待线程执行结束:

public final void join()
                throws InterruptedException

如我们可以这样使用:

import kotlin.concurrent.thread

fun main() {
    val th = thread {
        Thread.sleep(1000)
        println("th执行完成")
    }
    th.join()
    println("main执行完成")
}

 执行结果如下:

th执行完成
main执行完成

因此,join成功是main线程等待th线程结束

如果我们去掉th.join(),则输出:

main执行完成
th执行完成

这就是join()的基本用法

另外,如果当前线程(调用join()方法的线程)被任何线程中断,则抛出InterruptedException

异常,并不再等待:

import kotlin.concurrent.thread

fun main() {

    val th = thread {
        val th2 = thread {
            Thread.sleep(1000)
            println("th2执行完成")
        }
        try {
            th2.join()
        }catch (e: InterruptedException){
            println("中断")
        }
        println("th执行完成")
    }

    th.interrupt()

}

 执行结果:

中断
th执行完成
th2执行完成

因为main线程创建了th线程,th线程又创建了th2线程。th线程调用join()方法等待th2线程时,main线程中断了th线程,因此th线程中的join()方法停止等待,执行完成。之后,th2线程才执行完成

interrupt()

interrupt()中断线程。调用该方法时,将会把指定线程的Thread.interrupted()方法的返回值设为true,因此,要中断线程需要检测这个值。

public void interrupt()

 其用法如下:

import kotlin.concurrent.thread

fun main() {

    val th = thread {
        while (true){
            if (Thread.interrupted()) break
        }
        println("th被中断")
    }

    Thread.sleep(1000)
    println("准备中断线程")
    th.interrupt()

}

输出:

准备中断线程
th被中断

线程安全

线程安全必须同时满足原子性、可见性和有序性:

原子性

考虑这么一个代码:

import kotlin.concurrent.thread

fun main() {

    var tmp = 0

    val th1 = thread {
        Thread.sleep(200)
        tmp++
    }

    val th2 = thread {
        Thread.sleep(200)
        tmp++
    }

    th1.join()
    th2.join()

    println(tmp)

}

其中,tmp被增加了2次,因此应该返回2,可是我的输出结果为:

1

这是为什么呢?

我们知道,自增语句分三步:读取、增加、写入。在两个线程同时执行的时候,可能会出现类似以下情况:

时间第一个线程第二个线程
1读取tmp变量(0)
2计算tmp+1的值
3读取tmp变量(0)
4写入tmp+1的值到tmp变量(1)
5计算tmp+1的值
6写入tmp+1的值到tmp变量(1)

因此,由于线程之间并发运行,最终tmp的值为1。

之所以自增语句会出现这样的问题,是因为自增语句需要3块时间才能完成,不能一口气直接完成。如果自增可以直接完成,在非并行的情况下,就会出现以下情况:

时间第一个线程第二个线程
1tmp自增
2tmp自增

这样就不会有冲突了。

我们称这种直接完成而不被其他线程打断的操作叫原子操作,在kotlin中可以通过java.util.concurrent.atomic定义的支持原子操作的类,实现原子操作:

import java.util.concurrent.atomic.AtomicInteger
import kotlin.concurrent.thread

fun main() {

    val tmp = AtomicInteger(0)

    val th1 = thread {
        Thread.sleep(200)
        tmp.incrementAndGet()
    }

    val th2 = thread {
        Thread.sleep(200)
        tmp.incrementAndGet()
    }

    th1.join()
    th2.join()

    println(tmp.get())

}

注意:原子操作不适合并行时的问题,但由于现代电脑CPU少线程多的现状,大部分的情况都可以使用原子操作:

一个12核CPU有将近4000个线程

可见性

由于现代设备的线程有自己的缓存,有些时候当一个变量被修改后,其他线程可能看不到修改的信息,因此就会产生线程安全问题:

import kotlin.concurrent.thread

fun main() {

    var boolean = true

    val th1 = thread {
        Thread.sleep(200)
        boolean = false
        println("已经将boolean设为false")
    }

    val th2 = thread {
        println("等待boolean为false")
        while (boolean){}
    }

    th1.join()
    th2.join()

    println("线程执行完毕")

}

执行结果:

等待boolean为false
已经将boolean设为false
(无限循环)

 这是因为,当boolean被修改时,th2不能及时获得boolean的变化,所以跳不出循环,出现了可见性问题。我们可以通过Thread.yield()方法同步变量在线程内和进程内的数据:

import kotlin.concurrent.thread

fun main() {

    var boolean = true

    val th1 = thread {
        Thread.sleep(200)
        boolean = false
        println("已经将boolean设为false")
    }

    val th2 = thread {
        println("等待boolean为false")
        while (boolean){
            Thread.yield()
        }
    }

    th1.join()
    th2.join()

    println("线程执行完毕")

}

执行结果:

等待boolean为false
已经将boolean设为false
线程执行完毕

注意,Thread.yield()方法的真实作用是告诉调度器当前线程愿意放弃对处理器的使用,直到处理器重新调用这个线程,可以用以下表格来说明:

因此,Thread.yield()方法就可以抽空在合适的时机同步变量的数据,实现线程的可见性。

我们前面举的变量自增的例子也有可能是因为线程的可见性问题导致的。

有序性

我们在写代码时,往往认为程序是按顺序运行的,其实并不是。如果前后两个指令没有任何关联,处理器可能会先运行写在后面的省时指令,后运行写在前面的费时指令,这样可以起到节省资源的效果。在单线程中,这没有问题,但在多线程中,就出现了问题:

import kotlin.concurrent.thread

fun main() {

    var a = 0
    var b = 0
    var x = -1
    var y = -1

    var count = 0

    while (true) {

        a = 0
        b = 0
        x = -1
        y = -1

        val th1 = thread {
            b = 1
            x = a
            return@thread
        }

        val th2 = thread {
            a = 1
            y = b
            return@thread
        }

        th1.join()
        th2.join()

        count++

        if (x == 0 && y == 0){
            println("第$count 次,($x,$y)")
            break
        }

    }

}

输出:

第100010 次,(0,0)

按照正常的逻辑,这个程序的运行过程应该是类似这样的:

时间第一个线程第二个线程
1b=1
2a=1
3x=a(1)
4y=b(1)

时间第一个线程第二个线程
1b=1
2x=a(0)
3a=1
4y=b(1)

时间第一个线程第二个线程
1a=1
2y=b(0)
3b=1
4x=a(1)

无论如何,x和y都不可能同时为0,可是为什么原程序中,x和y都为0呢?

只有一种可能,类似这样:

x和y的赋值语句被处理器提到了前面,因此出现了有序性的问题 

@Volatile注解可以保证指定变量的可见性和有序性:

import kotlin.concurrent.thread

@Volatile
var a = 0

@Volatile
var b = 0

@Volatile
var x = -1

@Volatile
var y = -1

fun main() {

    var count = 0

    while (true) {

        a = 0
        b = 0
        x = -1
        y = -1

        val th1 = thread {
            b = 1
            x = a
            return@thread
        }

        val th2 = thread {
            a = 1
            y = b
            return@thread
        }

        th1.join()
        th2.join()

        count++

        if (x == 0 && y == 0) {
            println("第$count 次,($x,$y)")
            break
        }

    }

}

运行结果:

(无限循环)

 可见,@Volatile注解保证了其有序性。这个注解保证可见性和有序性的原理如下:

  • 可见性:给变量上一个Load屏障,每次读取数据的时候被强制从进程中读取最新的数据;同时上一个Store屏障,强制使每次修改之后强制刷新进程中的数据
  • 有序性:通过禁止重排屏障禁止指令重排:
    • StoreStore屏障:禁止StoreStore屏障的前后Store写操作重排
    • LoadLoad屏障:禁止LoadLoad屏障的前后Load读操作进行重排
    • LoadStore屏障:禁止LoadStore屏障的前面Load读操作跟LoadStore屏障后面的Store写操作重排
    • StoreLoad屏障:禁止LoadStore屏障前面的Store写操作跟后面的Load/Store 读写操作重排

线程锁

我们可以通过线程锁保证线程安全:

如果在操作对象之前,线程先声明:“这个对象是我的”,当另一个线程也想操作这个对象时,发现已经有人声明过了,那么它就等待,直到那个发布声明的线程又发了一个“这个对象不是我的了”的声明。当然,这两个声明其实起到了一个锁的作用,当声明“这个对象是我的”时,对象就被上了锁,当声明“这个对象不是我的了”时,对象的锁就被解开了

当然,上锁和解锁这一过程都必须保证原子性、可见性和有序性

我们可以如下修改代码:

import kotlin.concurrent.thread

class MyMutex{

    private var mutex: Boolean = false

    @Synchronized
    fun lock(){
        while (mutex){}  // 等待解锁
        mutex = true     // 上锁
        return
    }

    @Synchronized
    fun unlock(){
        mutex = false    // 解锁
        return
    }

}

fun main() {

    var tmp = 0
    val mutex = MyMutex()

    val th1 = thread {
        Thread.sleep(200)
        mutex.lock()
        tmp++
        mutex.unlock()
    }

    val th2 = thread {
        Thread.sleep(200)
        mutex.lock()
        tmp++
        mutex.unlock()
    }

    th1.join()
    th2.join()

    println(tmp)

}

这里面使用了@Synchronized注解,可以保证方法的原子性、可见性和有序性。在这里面保证了上锁和解锁的原子性、可见性和有序性。

@Synchronized注解是这么保证方法的原子性、可见性和有序性的:

  • 原子性:在方法执行前加锁,执行后解锁,这样一个方法同时只能有一个线程使用
  • 可见性:在方法执行时给方法内的每个变量上一个Load屏障,每次读取数据的时候被强制从进程中读取最新的数据;同时上一个Store屏障,强制使每次修改之后强制刷新进程中的数据
  • 有序性:通过禁止重排屏障禁止指令重排:
    • StoreStore屏障:禁止StoreStore屏障的前后Store写操作重排
    • LoadLoad屏障:禁止LoadLoad屏障的前后Load读操作进行重排
    • LoadStore屏障:禁止LoadStore屏障的前面Load读操作跟LoadStore屏障后面的Store写操作重排
    • StoreLoad屏障:禁止LoadStore屏障前面的Store写操作跟后面的Load/Store 读写操作重排

当然,我们上面的代码只是简单实现了一个线程锁,kotlin中可以使用自带的线程锁:

ReentrantLock

ReentrantLock是一个递归互斥,在Java中叫可重入锁,允许同一个线程多次上锁,相应的,同一个线程上锁多少次,就要解锁多少次。为什么要允许线程多次上锁呢?

我们来看以下代码:

import kotlin.concurrent.thread

class MyMutex{

    private var mutex: Boolean = false

    @Synchronized
    fun lock(){
        while (mutex){}  // 等待解锁
        mutex = true     // 上锁
        return
    }

    @Synchronized
    fun unlock(){
        mutex = false    // 解锁
        return
    }

}

fun main() {

    val mutex1 = MyMutex()
    val mutex2 = MyMutex()

    val th1 = thread {
        mutex1.lock()
        println("th1 locked mutex1")     // 模拟操作受mutex1保护的资源
        Thread.sleep(200)
        mutex2.lock()
        println("th1 locked mutex2")      // 模拟操作受mutex2保护的资源
        mutex1.unlock()
        println("th1 unlocked mutex1")
        Thread.sleep(200)
        mutex2.unlock()
        println("th2 unlocked mutex2")
        return@thread
    }

    val th2 = thread {
        mutex2.lock()
        println("th1 locked mutex2")      // 模拟操作受mutex2保护的资源
        Thread.sleep(200)
        mutex1.lock()
        println("th1 locked mutex1")      // 模拟操作受mutex1保护的资源
        mutex2.unlock()
        println("th1 unlocked mutex2")
        Thread.sleep(200)
        mutex1.unlock()
        println("th2 unlocked mutex1")
        return@thread
    }

    th1.join()
    th2.join()

    println("线程执行完毕")

}

运行结果:

th1 locked mutex1
th1 locked mutex2
(无限循环)

为什么会无限循环呢?因为th1锁定mutex1后想要锁定mutex2,却发现mutex2被th2锁定;而th2锁定mutex2后想要锁定mutex1,却发现mutex1被th1锁定,因此出现了无限循环的问题。我们称这种问题为死锁

使用递归互斥可以有效避免死锁问题:

import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.thread

fun main() {

    val mutex = ReentrantLock()

    val th1 = thread {
        mutex.lock()
        println("th1 locked mutex1")
        Thread.sleep(200)            // 模拟操作受mutex1保护的资源
        mutex.lock()
        println("th1 locked mutex2")      // 模拟操作受mutex2保护的资源
        mutex.unlock()
        println("th1 unlocked mutex1")
        Thread.sleep(200)
        mutex.unlock()
        println("th2 unlocked mutex2")
        return@thread
    }

    val th2 = thread {
        mutex.lock()
        println("th1 locked mutex2")      // 模拟操作受mutex2保护的资源
        Thread.sleep(200)
        mutex.lock()
        println("th1 locked mutex1")      // 模拟操作受mutex1保护的资源
        mutex.unlock()
        println("th1 unlocked mutex2")
        Thread.sleep(200)
        mutex.unlock()
        println("th2 unlocked mutex1")
        return@thread
    }

    th1.join()
    th2.join()

    println("线程执行完毕")

}

其中,代码

val mutex = ReentrantLock()

表示创建一个可重入锁,这个对象的lock()和unlock()方法分别表示上锁和解锁

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

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

相关文章

1298 - 摘花生问题

题目描述 Hello Kitty 想摘点花生送给她喜欢的米老鼠。她来到一片有网格状道路的矩形花生地(如下图),从西北角进去,东南角出来。地里每个道路的交叉点上都有种着一株花生苗,上面有若干颗花生,经过一株花生苗就能摘走该它上面所有…

Git命令操作

什么是Git? Git是⼀个免费的,开源的分布式版本控制软件系统 git区域 存储区域:Git软件⽤于存储资源得区域。⼀般指得就是.git⽂件夹 ⼯作区域:Git软件对外提供资源得区域,此区域可⼈⼯对资源进⾏处理。 暂存区&am…

Mendix 开发实践指南|Mendix的核心概念

在当今快速变化的技术环境中,Mendix平台以模型驱动开发方法,重新定义了应用程序的构建过程。本章内容,将深入探讨Mendix的几大核心概念:模型驱动开发、微流、纳流 、 实体模型和页面,旨在帮助我们全面理解Mendix平台的…

蓝桥杯-顺子日期

解答加解析 #include<iostream> using namespace std; int main() { //日期分别是:2022.01.20~29 10天 //10.12 11.23 12.30 12.31 //总体思路就是123 012 其他组合不能出现 cout<<"14"; return 0; }

全志H713/H618方案:调焦电机(相励磁法步进电机)的驱动原理、适配方法

一、篇头 全志H713平台&#xff0c;作为FHD投影的低成本入门方案&#xff0c;其公板上也配齐了许多投影使用的模组&#xff0c;本文即介绍投影仪调焦所用的步进电机&#xff0c;此模组的驱动原理、配制方法、调试方法。因为条件限制&#xff0c;本文采用的是H618香橙派Z3平台&…

模仿蜘蛛工作原理 苏黎世联邦理工学院研发牛油果机器人可在雨林树冠穿行

对于野外环境生物监测的研究人员来讲&#xff0c;收集生物多样性数据已成为日常工作重要组成部分&#xff0c;特别是对于热带雨林的茂密树冠当中活跃着非常多的动物、昆虫与植物。每次勘察都需要研究人员爬上茂密树冠收集数据&#xff0c;一方面增加了数据收集难度&#xff0c;…

Unity(第五部)新手图层和标签的理解

1、标记用于在物体上显示名字&#xff0c;方便开发 2、标签&#xff08;某一类物体&#xff0c;方便给某一类进行组件脚本编写&#xff09; 而且有了标签之后&#xff0c;我们在写代码的时候就可以直接通过标签找到一系列我们需要的游戏物体了 Untagged未标记Respawn重生Edi…

【hashmap】【将排序之后的字符串作为哈希表的键】【获取 HashMap 中所有值的集合】Leetcode 49 字母异位词分组

【hashmap】【将排序之后的字符串作为哈希表的键】【获取 HashMap 中所有值的集合】Leetcode 49 字母异位词分组 解法1 将排序之后的字符串作为哈希表的键解法2 在解法一的基础上加入了getOrDefault ---------------&#x1f388;&#x1f388;题目链接&#x1f388;&#x1f3…

在 where子句中使用子查询(二)

目录 ANY ANY &#xff1a;功能上与 IN 是没有任何区别的 >ANY &#xff1a;比子查询返回的最小值要大 ALL >AL &#xff1a;比子查询返回的最大值要大 EXISTS() 判断 NOT EXISTS Oracle从入门到总裁:https://blog.csdn.net/weixin_67859959/article/details/135209…

Matryoshka Representation Learning (MRL)-俄罗斯套娃向量表征学习

前言 在2024年1月底OpenAI发布新的向量模型&#xff0c;并提到新的向量模型支持将向量维度缩短。向量模型支持缩短维度而又不会威胁到向量的表示能力的原因在于使用了Matryoshka Representation Learning。 Matryoshka Representation Learning (MRL)是2022年发表的论文&#…

matlab 凸轮轮廓设计

1、内容简介 略 46-可以交流、咨询、答疑 2、内容说明 略 4 取标段的分析 取标装置是贴标机的核心部件之一&#xff0c;是影响贴标质量和贴标精度的重要因素&#xff0c;取标段是通过取标板与标签的相切运动使得涂有胶水的取标板从标签盒中粘取标签纸[4]&#xff0c;理论…

批量删除传参那些事

接口参数&#xff1a; public Object batchDeleteUsers(RequestBody List userIds) 工具提示传参&#xff1a; { “userIds”: [] } 错误&#xff01;&#xff01;&#xff01;讨逆猴子 报错&#xff1a;JSON parse error: Cannot deserialize value of type java.util.ArrayL…

Open3D 法向量的统一对齐定向 (26)

Open3D 法向量的统一对齐定向 (26) 一、算法介绍二、算法实现一、算法介绍 定向后:(法线方向统一朝向一个方向) 定向前:(法线的朝向是随机的,可能向下或者向上) 这两个方向都是正确的。这就是经常遇到的法线方向问题。定向即为将方向统一,更加规整 二、算法实现…

使用Node.js和Vue.js构建全栈Web应用

随着互联网的迅速发展&#xff0c;Web应用程序的开发变得越来越复杂和多样化。为了满足用户不断变化的需求&#xff0c;全栈开发已成为一个备受关注的话题。在本篇博客中&#xff0c;我将介绍如何使用Node.js和Vue.js来构建全栈Web应用。 Node.js是一个基于Chrome V8引擎的Jav…

C/C++暴力/枚举/穷举题目持续更新(刷蓝桥杯基础题的进!)

目录 前言 一、百钱买百鸡 二、百元兑钞 三、门牌号码&#xff08;蓝桥杯真题&#xff09; 四、相乘&#xff08;蓝桥杯真题&#xff09; 五、卡片拼数字&#xff08;蓝桥杯真题&#xff09; 六、货物摆放&#xff08;蓝桥杯真题&#xff09; 七、最短路径&#xff08;蓝…

【深入理解设计模式】代理设计模式

代理设计模式&#xff1a; 代理设计模式是一种结构型设计模式&#xff0c;它允许你提供一个替代物或占位符来控制对其他对象的访问。在代理模式中&#xff0c;一个类代表另一个类的功能。这种类型的设计模式属于结构型模式&#xff0c;因为该模式涉及类和对象的组合。 概述 …

【谈一谈】Redis是AP还是CP?

【谈一谈】Redis是AP还是CP? 再说这个话题之前,这里的是AP和CP不是"A片"和"C骗"啊 !~哈哈哈,就离谱,博文后面我会解释下的 我说下自己对Redis的感觉,我一直很好奇Redis,不仅仅是当缓存用那么简单,包括的它的底层设计 所以,思考再三,我决定先从Redis基础开…

最简单的基于 FFmpeg 的视音频分离器

最简单的基于 FFmpeg 的视音频分离器 最简单的基于 FFmpeg 的视音频分离器正文结果工程文件下载参考链接 最简单的基于 FFmpeg 的视音频分离器 参考雷霄骅博士的文章&#xff0c;链接&#xff1a;最简单的基于FFmpeg的封装格式处理&#xff1a;视音频分离器&#xff08;demuxe…

五种多目标优化算法(MOFA、NSWOA、MOJS、MOAHA、MOPSO)性能对比(提供MATLAB代码)

一、5种多目标优化算法简介 多目标优化算法是用于解决具有多个目标函数的优化问题的一类算法。其求解流程通常包括以下几个步骤&#xff1a; 1. 定义问题&#xff1a;首先需要明确问题的目标函数和约束条件。多目标优化问题通常涉及多个目标函数&#xff0c;这些目标函数可能…

电商风控系统(flink+groovy+flume+kafka+redis+clickhouse+mysql)

一.项目概览 电商的防止薅羊毛的风控系统 需要使用 groovy 进行风控规则引擎的编写 然后其它技术进行各种数据的 存储及处理 薅羊毛大致流程 如果单纯使用 if else在业务代码中进行风控规则的编写 那么 维护起来会比较麻烦 并且跟业务系统强绑定不合适 所以一般独立成一个单…