Kotlin智能强制转换vs空检查vs elvis操作符的线程安全性

li9yvcax  于 2023-10-23  发布在  Kotlin
关注(0)|答案(4)|浏览(141)

我看到过一些代码,

// value is a member of this class, so smart cast
// to non nullable doesn't happen
if (value != null) return value!!

因为value是类的成员,所以线程不能在检查和返回之间将值更改为null吗?从而导致撞车?
下面是一个更完整的例子,有两种不同的方法。在线程安全性方面,一个比另一个好吗?

class Example {
  private var cachedVariable: SomeClass? = null

  fun getCachedVariable(): SomeClass {
    // need to use !! because can't smart cast because 
    // mutable property.  But cant this crash if some 
    // other thread sets cachedVariable to null after 
    // this thread does the check?
    if (cachedVariable != null) return cachedVariable!!
    
    cachedVariable = SomeClass()
    return cachedVariable!!
  }
}

class Example2 {
  private var cachedVariable: SomeClass? = null

  fun getCachedVariable(): SomeClass {
    // Does this avoid the possible crash of Example 1
    return cachedVariable ?: SomeClass().also { 
      cachedVariable = it
    }
  }
}
pxy2qtax

pxy2qtax1#

你是对的,如果它是一个var,那么另一个线程可以在该行的中间修改它。这就是为什么这段代码只编译!!,因为编译器知道它不能确定值是否为null。这也关系到如何!!通常表示代码气味,你通常期望Kotlin中的空安全代码,但这个结构不是。这明确地意味着它可以崩溃。

rmbxnbpk

rmbxnbpk2#

因为value是类的成员,所以线程不能在检查和返回之间将值更改为null吗?从而导致撞车?
这是正确的,这也是为什么编译器在这里不会智能转换为非空值。你可以通过使用局部变量来解决这个问题:

val localValue = value
if (localValue != null) return localValue // no non-null assertion needed

对于你的第二个例子,也不是伟大的海事组织。如果这个字段只初始化一次,最好的选择是使用by lazy

private val cachedVariable by lazy { SomeClass() }

fun getCachedVariable(): SomeClass {
    return cachedVariable // the first call will initialize
}

如果出于某种原因,你不能或不想使用by lazy,你应该阅读Double-checked locking,这是安全和高性能的方法。

z9ju0rcb

z9ju0rcb3#

其他答案很好地解释了为什么Kotlin在这种情况下需要!! nullAssert。但通常,当你像这样访问一个示例变量时,你想在一个时刻捕获它的值,然后对捕获的值进行null检查。

class Example2 {
  private var cachedVariable: SomeClass? = null

  fun getCachedVariable(): SomeClass {
    val cachedVariableValue = cachedVariable
    if (cachedVariableValue == null) {
      // Do something
    } else {
      // Flow typing works here; the type of
      // cachedVariableValue is SomeClass now.
    }
  }
}

其他线程可以随时修改示例变量的值。但是如果你的函数创建了一个带有val的局部变量,那么 * 没有人 *,甚至是函数本身,可以再次修改这个值,所以如果你检查val是非空的,那么这个检查就保证永远是好的。
现在,在您的例子中,您还修改了cachedVariable,所以如果这是要在多线程环境中运行的代码,您需要使用互斥锁或监视器正确地锁定它。Java和Kotlin使用synchronized使这变得很容易,但你仍然必须这样做。

iugsix8n

iugsix8n4#

即使你的第二个例子是错误的,即使它不会崩溃。考虑以下操作序列
1.线程1进入getCachedVariablecachedVariable为null
1.线程2进入getCachedVariablecachedVariable为null
1.线程1创建SomeClass()并将其分配给cachedVariable
1.线程2创建SomeClass()并将其分配给cachedVariable
没有崩溃,但现在两个线程引用不同的对象,如果线程1对它的引用被删除,线程1的对象将被垃圾收集。
看起来你真正想要的是lazy

class Example {
  val cachedVariable by lazy { SomeClass() }
}

对于Example的每个示例,即使多个线程尝试访问它,也最多计算一次,并且所有线程都将安全地获得相同的示例。

相关问题