今天看看kotlin中的安全调用符的一个注意点。

之前的文章已经讲过kotlin中的安全调用符,可以点击查看。

知识点

kotlin中的安全调用符 ?. 是线程安全的。

代码验证

我们打开IDEA写下面一段代码:

class Sample(var name: String?){
    fun test(){
        if(name != null){
            println(name.length)
        }
    }
}

然后IDEA会提示你如下一段话:

Android kotlin进入子线程 kotlin线程安全list_安全调用符


简单一点说,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简单进行判断是线程不安全的。

写在最后

神秘的东西,揭开面纱以后,也就没那么神秘了。