今天看看kotlin中的安全调用符的一个注意点。
之前的文章已经讲过kotlin中的安全调用符,可以点击查看。
知识点
kotlin中的安全调用符 ?. 是线程安全的。
代码验证
我们打开IDEA写下面一段代码:
class Sample(var name: String?){
fun test(){
if(name != null){
println(name.length)
}
}
}
然后IDEA会提示你如下一段话:
简单一点说,IDEA认为name属性可能会在你判断完成之后,在你正式使用该变量之前被修改,也就是不是线程安全的。
想想也是,假如有一个Sample对象,被两个线程读取和修改,在不加锁的情况下,即使你if判断是非空的,但是等你正式使用的时候,可能已经被其他线程修改为null,这时候就会得到不期望的结果。
由于存在编译错误,无法在kotlin中演示上面的问题,我们在Java里面通过下面的代码演示一下:
public class Demo {
private String name;
public static void main(String[] args) throws InterruptedException {
Demo demo = new Demo();
demo.name = "name";
Thread thread1 = new Thread(() -> demo.name = null);
Thread thread2 = new Thread(() -> {
if (demo.name != null) {
try {
Thread.sleep(700);
System.out.println(demo.name.length());
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
System.out.println("deme.name is null");
}
});
thread2.start();
Thread.sleep(500);
thread1.start();
Thread.sleep(1000);
}
}
运行上面的代码,尽管已经通过了if判断,还是会抛出空指针异常,其实这就是简单的线程不安全的例子。
那kotlin的安全调用运算符是如何解决线程安全问题的呢?
我们可以尝试写一段最简单的代码,然后通过IDEA查看字节码,然后再从字节码反编译回Java代码进行查看。
我们先写下面一段代码:
class Sample(var name: String?) {
fun info() {
println("name len = " + name?.length)
}
}
我们通过点击工具栏的 Tools–>Kotlin–>Show Kotlin Bytecode 会在右侧打开一栏展示Kotlin的字节码,然后我们点击 Decompile 按钮就可以将字节码转为Java代码了,下面是转化后的Java代码:
import kotlin.Metadata;
import org.jetbrains.annotations.Nullable;
@Metadata(
mv = {1, 1, 15},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\u0016\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u0005\n\u0002\u0010\u0002\u0018\u00002\u00020\u0001B\u000f\u0012\b\u0010\u0002\u001a\u0004\u0018\u00010\u0003¢\u0006\u0002\u0010\u0004J\u0006\u0010\b\u001a\u00020\tR\u001c\u0010\u0002\u001a\u0004\u0018\u00010\u0003X\u0086\u000e¢\u0006\u000e\n\u0000\u001a\u0004\b\u0005\u0010\u0006\"\u0004\b\u0007\u0010\u0004¨\u0006\u0000"},
d2 = {"LSample;", "", "name", "", "(Ljava/lang/String;)V", "getName", "()Ljava/lang/String;", "setName", "info", ""}
)
public final class Sample {
@Nullable
private String name;
public final void info() {
StringBuilder var10000 = (new StringBuilder()).append("name len = ");
String var10001 = this.name;
String var1 = var10000.append(var10001 != null ? var10001.length() : null).toString();
boolean var2 = false;
System.out.println(var1);
}
@Nullable
public final String getName() {
return this.name;
}
public final void setName(@Nullable String var1) {
this.name = var1;
}
public Sample(@Nullable String name) {
this.name = name;
}
}
其实,我们只需要关注上面的 info() 函数,因为这里面使用了安全调用符,其实我们只需要关注下面两行即可:
String var10001 = this.name;
String var1 = var10000.append(var10001 != null ? var10001.length() : null).toString();
kotlin将我们的成员变量赋值给了var10001这个变量,后续的判断和使用都是去使用这个变量。
这样即使你其他线程修改了name属性的值,也就是将name这个引用指向了其他变量,也不会影响到var10001这个变量,var10001是个局部变量,在每个线程中都有一份自己的存在,是不共享的,自然就是线程安全的了。
顺便说一句,你会发现kotlin中下面的代码是不会报错:
fun sample(str: String?){
if(str != null){
println(str.length)
}
}
就是因为局部变量是线程安全的,只有上面演示的成员变量使用if简单进行判断是线程不安全的。
写在最后
神秘的东西,揭开面纱以后,也就没那么神秘了。