Kotlin多线程,Kotlin多线程编程技术解析

马肤

温馨提示:这篇文章已超过422天没有更新,请注意相关的内容是否还可用!

Kotlin多线程摘要:Kotlin是一种静态类型编程语言,支持多线程编程。它具有轻量级的线程实现和强大的并发处理能力。Kotlin的多线程特性允许开发者创建和管理多个线程,实现并发执行任务,提高程序性能和响应能力。通过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.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少线程多的现状,大部分的情况都可以使用原子操作:

    Kotlin多线程,Kotlin多线程编程技术解析 第1张 一个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()方法分别表示上锁和解锁


0
收藏0
文章版权声明:除非注明,否则均为VPS857原创文章,转载或复制请以超链接形式并注明出处。

相关阅读

  • 【研发日记】Matlab/Simulink自动生成代码(二)——五种选择结构实现方法,Matlab/Simulink自动生成代码的五种选择结构实现方法(二),Matlab/Simulink自动生成代码的五种选择结构实现方法详解(二)
  • 超级好用的C++实用库之跨平台实用方法,跨平台实用方法的C++实用库超好用指南,C++跨平台实用库使用指南,超好用实用方法集合,C++跨平台实用库超好用指南,方法与技巧集合
  • 【动态规划】斐波那契数列模型(C++),斐波那契数列模型(C++实现与动态规划解析),斐波那契数列模型解析与C++实现(动态规划)
  • 【C++】,string类底层的模拟实现,C++中string类的模拟底层实现探究
  • uniapp 小程序实现微信授权登录(前端和后端),Uniapp小程序实现微信授权登录全流程(前端后端全攻略),Uniapp小程序微信授权登录全流程攻略,前端后端全指南
  • Vue脚手架的安装(保姆级教程),Vue脚手架保姆级安装教程,Vue脚手架保姆级安装指南,Vue脚手架保姆级安装指南,从零开始教你如何安装Vue脚手架
  • 如何在树莓派 Raspberry Pi中本地部署一个web站点并实现无公网IP远程访问,树莓派上本地部署Web站点及无公网IP远程访问指南,树莓派部署Web站点及无公网IP远程访问指南,本地部署与远程访问实践,树莓派部署Web站点及无公网IP远程访问实践指南,树莓派部署Web站点及无公网IP远程访问实践指南,本地部署与远程访问详解,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南。
  • vue2技术栈实现AI问答机器人功能(流式与非流式两种接口方法),Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法探究,Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法详解
  • 发表评论

    快捷回复:表情:
    评论列表 (暂无评论,0人围观)

    还没有评论,来说两句吧...

    目录[+]

    取消
    微信二维码
    微信二维码
    支付宝二维码