【问题标题】:How does thread synchronization work in Kotlin?Kotlin 中的线程同步是如何工作的?
【发布时间】:2020-09-01 19:21:10
【问题描述】:

我一直在尝试 Kotlin 同步,但我从文档中不了解锁定机制如何在公共资源上的线程同步上起作用,因此试图编写这段代码,这进一步使我的理解复杂化。

fun main() {
    val myList = mutableListOf(1)

    thread {
        myList.forEach {
            while (true) {
                println("T1 : $it")
            }
        }
    }

    thread {
        synchronized(myList) {
            while (true) {
                myList[0] = 9999
                println("**********\n**********\n**********\n")
            }
        }
    }
}

myList 是有问题的公共资源。

第一个线程是一个简单的读取操作,我打算将使用的资源保持在读取模式。第二个是另一个请求锁以修改公共资源的线程。

虽然第一个线程不包含任何同步,但我希望它能够在内部处理这个问题,以便当像 mapforEach 这样的函数在资源上运行时,另一个线程不应该能够锁定否则被迭代的元素可能会在 map/forEach 正在进行时发生变化(即使该操作可能会暂停一段时间,而另一个线程对其进行锁定)。

我看到的输出显示两个线程并行执行。他们都分别打印列表中的第一个元素和星星。但是在第二个线程中,即使正在打印星星,myList[0] 也永远不会设置为 9999,因为第一个线程继续打印 1。

【问题讨论】:

  • 它不会“在内部处理这个”。第一个线程根本没有保护任何东西。
  • 你需要通过happens-before来了解和应用并发推理。 Java 没有提供开箱即用的“最终一致性”,您似乎暗示了这一点。没有happens-before,就没有一致性。

标签: java kotlin synchronization kotlin-coroutines kotlin-android-extensions


【解决方案1】:

线程和同步是 JVM 特性,不是 Kotlin 特有的。如果您可以学习 Java,那么有很多资源可以充分解释它们。但简短的回答是:它们非常低级,而且很难正确处理,所以请谨慎行事。如果更高级别的构造(工作队列/执行器、map/reduce、actor ......)或不可变对象可以满足您的需求,那么如果您使用它,生活会更轻松!

但这里是基础知识。首先,在 JVM 中,每个对象都有一个锁,可以用来控制对某物的访问。 (这通常是锁所属的对象,但不一定是......)锁可以由特定线程中的代码获取;当它持有那个锁时,任何试图获取锁的other线程都会阻塞,直到第一个线程释放它。

这几乎就是全部了! synchronised 关键字(实际上是一个函数)用于声明锁;属于给定对象或(如果没有给出)“this”对象。

注意持有锁会阻止其他线程持有锁;它不会阻止其他任何事情。所以恐怕你的期望是错误的。这就是为什么您会看到线程愉快地同时运行。

理想情况下,编写每个类时都会考虑到它如何与多线程交互;它可以将自己记录为“不可变”(无需担心可变状态)、“线程安全”(同时从多个线程调用安全)、“有条件线程安全”(如果遵守某些模式,则可以从多个线程安全调用to)、“线程兼容”(不采取特殊预防措施,但调用者可以自己进行同步以使其安全)或“线程敌对”(不可能从多个线程中使用)。但实际上,大多数人不会。

事实上,大多数都是线程兼容的;这适用于大部分 Java 和 Kotlin 集合类。因此,您可以进行自己的同步(根据您的 synchronized 块);但是您必须注意同步每个对列表的可能访问——否则,竞争条件可能会使您的列表处于不一致的状态。

(这不仅仅意味着某个地方的狡猾值。我有一个服务器应用程序的线程卡在繁忙循环中 - 占用了 100% 的 CPU,但从不继续执行其余代码-- 因为我有一个线程更新 HashMap 而另一个线程正在读取它,而我错过了其中一个线程的同步。最尴尬。)

所以,正如我所说,如果您可以改用更高级别的构造,您的生活会更轻松!

【讨论】:

  • 一个更高层次的结构确实是我想要达到的。但是由于 map、forEach 和 any 等函数被注入到 Collections 中,我不确定我是否理解覆盖它们的方法。这也让我担心,如果假设更新带来了一个新函数来迭代集合。使用我的 List 实现以及从 Collection 注入的新函数的人将无法愉快地进行同步。
  • 恐怕我不明白你的担心。如果您使用某种更高级别的构造,那将是而不是您的基本集合(或控制对它的访问)。有些实现确实有一些并发保证,例如CopyOnWriteArrayListConcurrentHashMap,所以你可以随意使用它们……(续)
  • ... 但是它们有自己的开销和怪癖,这就是为什么 Collections 接口本身没有指定任何关于并发性的原因。最终,如果不了解您想要达到的目标,就很难提出建议。
  • 打个比方,我正在创建自己的同步列表实现。这实现了 List 接口。现在,即使我有自己的实现,也有很多地方可以将实用程序函数注入到 List 中,以至于我无法在我的实现中覆盖它们中的每一个。所以我不知道如何进行更高级别的建设。
  • “注入”是什么意思?如果您要扩展,例如AbstractList,那么你就会从那里得到方法;但只有当你对它的方法感到满意时,你才应该这样做。否则,您可能不应该扩展任何内容(如果需要,请保留对另一个 List 的引用,您可以根据需要委托给它)。你也可以从你正在实现的接口中获取默认方法,并且有扩展方法——但这些方法都不能直接访问你的任何状态,所以它们应该不是问题。
【解决方案2】:

第二个线程不会改变第一个列表元素的值,因为== 表示比较,而不是分配。您需要使用 = tio 更改值,例如myList[0] = 9999。但是,在您的代码中,不能保证来自第二个线程的更改将在第一个线程中变得可见,因为线程一个未在 myList 上同步。

如果您的目标是 JVM,您应该阅读有关 JVM 内存模型的信息,例如什么是@Volatile。您当前的方法不能保证第一个线程会看到第二个线程的更改。您可以将代码简化为以下 broken 示例:

var counter = 1

fun main() {
    thread {
        while (counter++ < 1000) {
            println("T1: $counter")
        }
    }
    thread {
        while (counter++ < 1000) {
            println("T2: $counter")
        }
    }
}

这会打印出奇怪的结果,例如:

T2: 999
T1: 983
T2: 1000

这可以通过几种方式解决,例如通过使用同步。

【讨论】:

  • 我更正了赋值运算符。我在应该使用赋值的地方错误地使用了相等检查。
  • 我真正想弄清楚的是,像 forEach、map、any 这样的操作是相当长时间运行的操作。那么如果同步进程在这样的 map/forEach/any 函数正在进行时请求锁定会发生什么?是授予了锁并且这些函数暂停了一段时间,还是直到这些函数完成执行才授予锁?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-13
  • 2010-10-19
  • 2012-02-19
  • 2021-01-31
  • 2015-06-15
相关资源
最近更新 更多