Kotlin多线程

目录

线程的使用

线程的创建

例一:创建线程并输出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.threadfun 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.threadfun 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.threadfun 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.threadfun 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.threadfun main() {val th = thread {while (true){if (Thread.interrupted()) break}println("th被中断")}Thread.sleep(1000)println("准备中断线程")th.interrupt()}

输出:

准备中断线程
th被中断

线程安全

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

原子性

考虑这么一个代码:

import kotlin.concurrent.threadfun main() {var tmp = 0val 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.threadfun 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.threadfun main() {var boolean = trueval th1 = thread {Thread.sleep(200)boolean = falseprintln("已经将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.threadfun main() {var boolean = trueval th1 = thread {Thread.sleep(200)boolean = falseprintln("已经将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.threadfun main() {var a = 0var b = 0var x = -1var y = -1var count = 0while (true) {a = 0b = 0x = -1y = -1val th1 = thread {b = 1x = areturn@thread}val th2 = thread {a = 1y = breturn@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 = -1fun main() {var count = 0while (true) {a = 0b = 0x = -1y = -1val th1 = thread {b = 1x = areturn@thread}val th2 = thread {a = 1y = breturn@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.threadclass MyMutex{private var mutex: Boolean = false@Synchronizedfun lock(){while (mutex){}  // 等待解锁mutex = true     // 上锁return}@Synchronizedfun unlock(){mutex = false    // 解锁return}}fun main() {var tmp = 0val 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.threadclass MyMutex{private var mutex: Boolean = false@Synchronizedfun lock(){while (mutex){}  // 等待解锁mutex = true     // 上锁return}@Synchronizedfun 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.threadfun 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://xiahunao.cn/news/2810028.html

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

相关文章

《Docker 简易速速上手小册》第7章 高级容器管理(2024 最新版)

文章目录 7.1 容器监控与日志7.1.1 重点基础知识7.1.2 重点案例:监控 Flask 应用7.1.3 拓展案例 1:使用 ELK Stack 收集和分析日志7.1.4 拓展案例 2:使用集成监控工具 7.2 性能调优与资源限制7.2.1 重点基础知识7.2.2 重点案例:Fl…

【福建游戏业:低调崛起的区域力量】

在前面的文章中,我们已经依次介绍了上海、北京、广州、深圳、成都、杭州等地的游戏行业发展现状。本文要向大家介绍的最后一个地区--福建。 尽管福建省的经济总体实力相对较弱,但近年游戏业焕发出勃勃生机,涌现出几家优秀游戏企业&#xff0c…

airserver2024mac苹果手机电脑投屏工具下载

AirServer的稳定性如磐石般坚固,当提及投屏软件的核心要素时,稳定性无疑是用户最为关心的方面之一。在这方面,AirServer堪称投屏领域的佼佼者,其稳定性表现足以让用户放心依赖。 首先,AirServer采用了先进的投屏技术&…

文件夹批量字符串检索工具

文件夹 文件夹批量字符串检索工具是一种使用于在指定文件夹中批量搜索指定字符串的工具。它可以帮助用户快速找到包含特定字符串的文件,并提供相应的搜索结果。 这种工具通常具有以下功能: 批量搜索:用户可以指定一个文件夹,在该…

HTML(待完善)

typora-copy-images-to: img 前端api: https://www.w3school.com.cn/ 1.前端知识介绍(了解) 4天内容比较细 碎 多。 小结: 前端知识点不需要单独安装特有的软件。只需要浏览器即可。谷歌 、火狐、IE. 网站后台前端网页 2.HTML的概述(了解) 1.HTML应用场景 各大…

ChatRTX安装教程

介于本人一直想将现有的智慧城市的文档结合大模型RAG实现知识库问答助手,借着Chat With RTX的风潮正好将机器人和知识库合二为一,方便以后对众多文件进行查阅。 一、概要 Chat With RTX 是一个 Demo,用来将您自己的资料(文档、笔…

事务隔离大揭秘:MySQL中的四种隔离级别解析

欢迎来到我的博客,代码的世界里,每一行都是一个故事 事务隔离大揭秘:MySQL中的四种隔离级别解析 前言事务概述mysql隔离级别并发问题与隔离级别关系事务隔离级别的配置与设置 前言 在当今数据驱动的世界中,数据库事务的一致性和隔…

常见的音频与视频格式

本专栏是汇集了一些HTML常常被遗忘的知识,这里算是温故而知新,往往这些零碎的知识点,在你开发中能起到炸惊效果。我们每个人都没有过目不忘,过久不忘的本事,就让这一点点知识慢慢渗透你的脑海。 本专栏的风格是力求简洁…

查看Android中正在运行的程序包名

要想知道正在运行程序是什么,可以先把程序退出,然后在Logcat中过滤消息包含displayed即可,如下: 还可以使用TAG为ActivityTaskManager进行过滤,如下: 这样过滤的结果比较多,所以还是用display…

SpringCloud Alibaba 2022之Nacos学习

SpringCloud Alibaba 2022使用 SpringCloud Alibaba 2022需要Spring Boot 3.0以上的版本,同时JDK需要是17及以上的版本。具体的可以看官网的说明。 Spring Cloud Alibaba版本说明 环境搭建 这里搭建的是一个聚合项目。项目结构如下: 父项目的pom.xm…

03-Linux权限

root用户 root用户(超级管理员) 无论是Windows、MacOS、Linux均采用多用的管理模式进行权限管理 在Linux系统中,拥有最大权限的账户名为:root(超级管理员)刚开始学习的时候,大多时间都是用的…

曲线生成 | 图解B样条曲线生成原理(附ROS C++/Python/Matlab仿真)

目录 0 专栏介绍1 控制点计算之插值2 控制点计算之近似3 仿真实现3.1 ROS C实现3.2 Python实现3.3 Matlab实现 0 专栏介绍 🔥附C/Python/Matlab全套代码🔥课程设计、毕业设计、创新竞赛必备!详细介绍全局规划(图搜索、采样法、智能算法等)&a…

用于模拟颗粒流的直接强迫浸没边界法 An immersed boundary method with direct forcing 笔记

原文:Uhlmann, Markus. “An immersed boundary method with direct forcing for the simulation of particulate flows.” Journal of computational physics 209.2 (2005): 448-476. 目录 概述引言问题表述固体对流体的作用欧拉和拉格朗日变量的空间离散体积力的表…

【Leetcode】235. 二叉搜索树的最近公共祖先

文章目录 题目思路代码结果 题目 题目链接 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度…

Python爬虫-爬取B站番剧封面

本文是本人最近学习Python爬虫所做的小练习。如有侵权,请联系删除。 页面获取url 代码 import requests import os import re# 创建文件夹 path os.getcwd() /images if not os.path.exists(path):os.mkdir(path)# 当前页数 page 1 # 总页数 total_page 2# 自动…

基于ELFBoard开发板的车牌识别系统

本项目采用的是ElfBoard ELF1开发板作为项目的核心板,主要实现的功能为通过USB 摄像头对车牌进行识别,如果识别成功则会亮绿灯,并将识别的车牌号上传到手机APP上面,车牌识别的实现是通过百度OCR进行实现,手机APP是用Ja…

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

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

Linux基础命令—系统服务

基础知识 centos系统的开机流程 1)通电 2)BIOS硬件检查 3)MBR引导记录 mbr的引导程序 加载引导程序 让硬件加载操作系统内核 MBR在第一个磁盘第一个扇区 总大小512字节 mbr: 1.引导程序: 占用446字节用于引导硬件,加载引导程序 2.分区表: 总共占…

数学建模【插值与拟合】

一、插值与拟合简介 在数学建模过程中,通常要处理由试验、测量得到的大量数据或一些过于复杂而不便于计算的函数表达式,针对此情况,很自然的想法就是,构造一个简单的函数作为要考察数据或复杂函数的近似。插值和拟合就可以解决这…

GitHub上的GCN

在GitHub上下载GCN代码,可以跑通 https://github.com/tkipf/pygcn