使用kotlin 协程调用java synchronized 同步方法会出现安全问题吗?

28 阅读3分钟

先说结论:不会有安全性的问题。原因是kotlin协程在执行synchronized代码块时不会让出cpu(只有执行suspend函数时才会让出cpu被调度),会一直执行下去。另外kotlin的synchronized关键字无法修饰suspend函数,协程在执行synchronized修饰的kotlin函数时也不会被调度。所以不会产生安全性的问题。

起因是开发中遇到了这样的问题:app上层代码使用了kotlin,而依赖的库是用java写的。java代码中使用了synchronized关键字控制线程同步。如果同一个线程中的多个协程同时调用java的同步方法会产生安全性的问题吗?

网上的说法是synchronized关键字在单线程上的多个协程之间仍然有效。这个结论与我最初的想法相反,synchronized关键字控制线程之间的并发,如果多个协程运行在同一个线程之中,synchronized关键字应该无法控制才对。

于是我做了下面的一些实验:

1. 多协程并发访问synchronized方法

public class SyncTest {
    private static final String TAG = "SyncTest";
    void doSomethingSync(int index) {
        synchronized (this) {
            Log.d(TAG, "doSomething: start index ="+index+" ThreadName = " + Thread.currentThread().getName() + "");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            Log.d(TAG, "doSomething: end index ="+index+" ThreadName = " + Thread.currentThread().getName() + "");
        }
    }
}
@OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class)
fun test1() {
    val dispatcher = newSingleThreadContext("syncTestThread")
    val syncTest = SyncTest()

    repeat(10) {index->
        CoroutineScope(dispatcher).launch {
            syncTest.doSomethingSync(index)
        }
    }
}

实验结果:

doSomething: start index =0 ThreadName = syncTestThread
doSomething: end index =0 ThreadName = syncTestThread
doSomething: start index =1 ThreadName = syncTestThread
doSomething: end index =1 ThreadName = syncTestThread
doSomething: start index =2 ThreadName = syncTestThread
doSomething: end index =2 ThreadName = syncTestThread
doSomething: start index =3 ThreadName = syncTestThread
doSomething: end index =3 ThreadName = syncTestThread
doSomething: start index =4 ThreadName = syncTestThread
doSomething: end index =4 ThreadName = syncTestThread
doSomething: start index =5 ThreadName = syncTestThread
doSomething: end index =5 ThreadName = syncTestThread
doSomething: start index =6 ThreadName = syncTestThread
doSomething: end index =6 ThreadName = syncTestThread
doSomething: start index =7 ThreadName = syncTestThread
doSomething: end index =7 ThreadName = syncTestThread
doSomething: start index =8 ThreadName = syncTestThread
doSomething: end index =8 ThreadName = syncTestThread
doSomething: start index =9 ThreadName = syncTestThread
doSomething: end index =9 ThreadName = syncTestThread

可以看到10个协程依次执行了doSomethingSync方法,不存在方法执行的过程中有其他协程进入同步方法块的情况。

2. 多协程并发访问非synchronized方法

void doSomething(int index) {
    synchronized (this) {
        Log.d(TAG, "doSomething: start index ="+index+" ThreadName = " + Thread.currentThread().getName() + "");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        Log.d(TAG, "doSomething: end index ="+index+" ThreadName = " + Thread.currentThread().getName() + "");
    }
}
fun test2() {
    val syncTest = SyncTest()
    repeat(10) {index->
        syncTest.doSomething(index)
    }
}

实验结果

doSomething: start index =0 ThreadName = syncTestThread
doSomething: end index =0 ThreadName = syncTestThread
doSomething: start index =1 ThreadName = syncTestThread
doSomething: end index =1 ThreadName = syncTestThread
doSomething: start index =2 ThreadName = syncTestThread
doSomething: end index =2 ThreadName = syncTestThread
doSomething: start index =3 ThreadName = syncTestThread
doSomething: end index =3 ThreadName = syncTestThread
doSomething: start index =4 ThreadName = syncTestThread
doSomething: end index =4 ThreadName = syncTestThread
doSomething: start index =5 ThreadName = syncTestThread
doSomething: end index =5 ThreadName = syncTestThread
doSomething: start index =6 ThreadName = syncTestThread
doSomething: end index =6 ThreadName = syncTestThread
doSomething: start index =7 ThreadName = syncTestThread
doSomething: end index =7 ThreadName = syncTestThread
doSomething: start index =8 ThreadName = syncTestThread
doSomething: end index =8 ThreadName = syncTestThread
doSomething: start index =9 ThreadName = syncTestThread
doSomething: end index =9 ThreadName = syncTestThread

虽然没有synchronized关键字,10个协程依然依次执行了doSomething方法,可见Thread.sleep操作并不会使当前协程被调度。(因为协程不会被强制中断,只有在遇到suspend函数时才会主动让出执行权

那如果使用synchronized关键字的是kotlin代码,synchronized修饰的方法块中调用了suspend函数会怎么样?

3. 在kotlin中使用Synchronized关键字修饰suspend方法

我写了下面的代码准备进行测试,结果编译器报错。

class KotlinSyncTest()  {

    @Synchronized//这里会报错:@Synchronized annotation is not applicable to suspend functions and lambdas
    suspend fun doSomething(index: Int) {
        repeat(10) {
            println("doSomething $index")
            delay(100)
        }
    }

}

在@Synchronized这行报错:@Synchronized annotation is not applicable to suspend functions and lambdas。意思是:Synchronized注释不适用于suspend函数和 lambda表达式。kotlin从语法上避免了Synchronized修饰的函数在被协程执行时由于协程调度而引起的同时访同步代码块的问安全性问题。