The above code has been uploaded to Github, ball ball old buddy’s Star

preface

In real development, there are many places to ensure thread safety and mutual exclusion. Locking is a good way to do this. What are some ways to achieve thread-safe effects in Kotlin?

Why do you need to deal with thread-safety issues

Thread-safety issues are caused by global and static variables. When multiple threads write to the same object at the same time, each thread may read different values. In short, if variables are not written in the thread and values are read, you can avoid thread safety.

lock

Locks can be classified into instance locks and global locks. Common methods are Synchronized keyword and Lock method

  • Instance lock: A lock on an object that is a singleton of its class will also act as a global lock
  • Global lock: if a lock is applied to a class, all objects in that class will be locked, and the thread holding the class lock will hold the locks on all objects

1. Synchronized

Synchronized is a keyword in Java and an annotation class in Kotlin

/** * Marks the JVM method generated from the annotated function as `synchronized`, meaning that the method * will be protected from concurrent execution by multiple threads by the monitor of the instance  (or, * for static methods, the class) on which the method is defined. */ @Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) @Retention(AnnotationRetention.SOURCE) @MustBeDocumented public actual annotation class SynchronizedCopy the code

Let’s review the Synchronized keyword in Java:

The Synchronized keyword ensures that a locked object can be executed by only one thread at a time in a multi-threaded environment, and can only be acquired by another thread after the current thread releases the lock on the object. Synchronized also guarantees visibility of shared variables.

Ground rules:

  • With normal methods, the current object is locked
  • For static methods, the class class is locked
  • For a code block, the lock is the object in the code block

When one thread accesses an object’s synchronized area, other threads accessing the synchronized area block, but other threads can access the object’s non-synchronized area.

1.1 Use of synchronized in Java

Have a little

1.2 Use of synchronized in Kotlin

Lock the method


A @synchronized annotation is all we need to do to lock a method. For example, we define the following method:

@synchronized private fun postResult(s: String){println("${system.currentTimemillis ()} : $s") sleep(5000)}Copy the code

Call this method in a different thread:

fun testFunSync() {
    Thread{
        postResult("first thread")
    }.start()

    Thread{
        postResult("second thread")
    }.start()
}
Copy the code

The first thread calls the postResult method, blocking for 5s after printing the current time. It takes 5 seconds for the second thread to acquire the lock for postResult.

Output results:

1622794297695 ��first thread 1622794302698 ��second threadCopy the code

What if we block the first thread, what happens when the lock is acquired?

Fun testFunSync() {Thread{postResult("first Thread ")}.start() Thread{ postResult("second thread") }.start() }Copy the code
1622794450105 ��first thread
1622794455107 ��second thread

Copy the code

The output is still the same. The first thread will release the lock after playing postResult, and the subsequent actions will not affect the second thread’s capture of the postResult lock


To lock

First we create a Result class:

class Result { var s : String = "" public fun printResult(){println("${system.currentTimemillis ()} : $s")}}Copy the code

call

fun testClassSync() {
    val result= Result()


    Thread{
        synchronized(result){
            result.s = currentThread().name
            result.printResult()
        }
    }.apply {
        name = "first thread"
        start()
    }

    Thread{
        synchronized(result){
            result.s = currentThread().name
            result.printResult()
        }
    }.apply {
        name = "second thread"
        start()
    }

}
Copy the code

Output results:

1622795509253 ��first thread 1622795514269 ��second thread // If the time difference is 5s, the lock takes effectCopy the code

That’s locking the Result class object. What if there are multiple instances of the Result class? We modify the call method slightly:

Fun testClassSync() {val result2= result () Thread{synchronized(result){resul.s  = currentThread().name result.printResult() } }.apply { name = "first thread" start() } Thread{ synchronized(result2){ result2.s = currentThread().name result2.printResult() } }.apply { name = "second thread" start() } }Copy the code

Create multiple Result objects. First thread locks A object, but second thread locks B object.

First thread 1622795623480 ��second threadCopy the code

If we lock the Result class, all objects will share the same lock:

Fun testClassSync() {val result= result () val result2= result () Thread{synchronized(result ::class.java){ result.s = currentThread().name result.printResult() } }.apply { name = "first thread" start() } Thread{ synchronized(Result::class.java){ result2.s = currentThread().name result2.printResult() } }.apply { name = "second thread" start() } }Copy the code

Output results:

1622796124320 ��first thread
1622796129336 ��second thread
Copy the code

Although the first and second threads call different objects, the first thread has a class lock on the Resutl class and the second thread has to wait.

The output indicates that when a class lock is added to xx::class. Java, all objects in that class will share the same lock.


2.Lock

Lock is an interface. The common implementation class is ReentrantLock, which means a ReentrantLock.

We define a global variable that writes to both threads simultaneously and starts both threads at the same time

fun testLockSync() { var count = 0 val thread1 = Thread{ for (i in 0.. 1000){ count += i } println("${Thread.currentThread().name} : count:${count}") } val thread2 = Thread{ for (i in 0.. 1000){ count += i } println("${Thread.currentThread().name} : ${count}")} ${count}")}Copy the code

Output: You will find that the output is different each time

Thread-1 : count:505253
Thread-2 : count:1001000
---
Thread-2 : count:1001000
Thread-1 : count:1001000
---
Thread-2 : count:822155
Thread-1 : count:822155
Copy the code

Use ReentrantLock to ensure thread safety:

fun testLockSync() { val lock = ReentrantLock() var count = 0 val thread1 = Thread{ lock.lock() for (i in 0.. 1000){ count += i } println("${Thread.currentThread().name} : count:${count}") lock.unlock() } val thread2 = Thread{ lock.lock() for (i in 0.. 1000){ count += i } println("${Thread.currentThread().name} : count:${count}") lock.unlock() } thread1.start() thread2.start() }Copy the code

The output and execution order are the same each time:

Thread-1 : count:500500
Thread-2 : count:1001000
Copy the code

Lock common methods

  • Lock () : obtain the lock, obtain successfully set the current thread count++, if another thread has the lock, the current thread is unavailable, wait.
  • TryLock () : Gets the lock. If the lock is not available, this method returns immediately. The thread does not block and the wait time can be set.
  • UnLock () : Attempts to release the lock, current thread count–, if count is 0, the lock is released.

2.Object

Object has many functions in Kotlin. You can implement object expressions, object declarations, and so on

An Object expression creates an Object class that implements the singleton pattern:


object Singleton {

}
Copy the code

View its decompiled Java code in IDEA:

public final class Singleton { public static final Singleton INSTANCE; private Singleton() { } static { Singleton var0 = new Singleton(); INSTANCE = var0; }}Copy the code

As you can see, it belongs to han mode: the advantage is that it is created when the class is loaded, so there is no thread safety problem, and it is high efficiency. The downside is a waste of resources. See this link for details

But the methods defined in the Object class are not thread-safe

We define the following methods:

object Singleton {
    
    fun printS(){
        println("${System.currentTimeMillis()} : ${Thread.currentThread().name}")
        sleep(1000)
        println("${System.currentTimeMillis()} : ${Thread.currentThread().name}")
    }
}
Copy the code

Call:

fun testObjectSync() {

    val thread1 = thread(start = false,name = "first thread"){
        Singleton.printS()
    }

    val thread2 = thread(start = false,name = "second thread"){
        Singleton.printS()
    }

    thread1.start()
    thread2.start()
}
Copy the code

Output:

1623036691322 : first thread
1623036691322 : second thread
1623036692329 : second thread
1623036692329 : first thread
Copy the code

We found that both threads were able to retrieve the object at the same time, and the output was almost the same time, without the sleep effect.

You still need to add @synchronized to achieve thread safety, as follows:

object Singleton { @Synchronized fun printS(){ println("${System.currentTimeMillis()} : ${Thread.currentThread().name}") sleep(1000) println("${System.currentTimeMillis()} : ${thread.currentthread ().name}")}} ${thread.currentthread ().name}")}} second thread 1623036785476 : second threadCopy the code

3. By lazy

Object is simple enough to implement the singleton mode, but Objcet cannot initialize parameters. We can create a singleton class with parameters using the lazy property:

class SomeSingleton(s:String) {

    companion object{

        private var s = ""

        private val instance : SomeSingleton by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
            SomeSingleton(s)
        }

        fun getInstances(string: String):SomeSingleton{
            this.s = string
            return instance
        }
    }

    fun printS(){
        println("${System.currentTimeMillis()} : ${Thread.currentThread().name} -------   s is:${s}")
        Thread.sleep(5000)
    }

}
Copy the code

Pay attention to here by lazy (mode = LazyThreadSafetyMode. SYNCHRONIZED)

SYNCHRONIZED by default in order to ensure that only one thread can uninitialize the lazy property. In other words, if multiple threads access the delay property at the same time, other threads will not be able to access it if it is not initialized properly.

Lazy load lock implementation source:

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable { private var initializer: (() -> T)? = initializer @Volatile private var _value: Any? = UNINITIALIZED_VALUE // final field is required to enable safe publication of constructed instance private val lock = lock ? : this override val value: T get() { val _v1 = _value if (_v1 ! == UNINITIALIZED_VALUE) { @Suppress("UNCHECKED_CAST") return _v1 as T } return synchronized(lock) { val _v2 = _value if (_v2 ! == UNINITIALIZED_VALUE) { @Suppress("UNCHECKED_CAST") (_v2 as T) } else { val typedValue = initializer!! () _value = typedValue initializer = null typedValue } } } override fun isInitialized(): Boolean = _value ! == UNINITIALIZED_VALUE override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet." private fun writeReplace(): Any = InitializedLazyImpl(value) }Copy the code

Also, the methods in SomeSingleton are not thread-safe and need to be added @synchorized, so I won’t go into that here.

4. Thread safety in coroutines

The coroutine provides Mutex to ensure mutual exclusion, as an alternative to Synchorinzed and Lock, and the withLock extension function is a convenient alternative to the common:

    mutex.lock()
    try {
        //do something
    }finally {
        mutex.unlock()
    }
Copy the code

Replace with:

    mutex.withLock {
        //do something
    }
Copy the code

Specific source:

@OptIn(ExperimentalContracts::class)
public suspend inline fun <T> Mutex.withLock(owner: Any? = null, action: () -> T): T {
    contract { 
        callsInPlace(action, InvocationKind.EXACTLY_ONCE)
    }

    lock(owner)
    try {
        return action()
    } finally {
        unlock(owner)
    }
}
Copy the code

Let’s look at the specific use:

suspend fun testMutex() { var count = 0 val job1 = CoroutineScope(Dispatchers.IO).launch{ repeat(100){ count ++ //delay Delay (1)} println("count1:${count}")} val job2 = CoroutineScope(Dispatchers.IO).launch{repeat(100){ count ++ delay(1) } println("count2:${count}") } job1.join() job2.join() }Copy the code

We run it several times, and each time the output is different:

count2:196
count1:196
Copy the code

Let’s add Mutex and try it: note that the same Mutex is used for multiple coroutines

Suspend fun testMutex() {var count = 0 Val Mutex = Mutex() val job1 = CoroutineScope(Dispatchers.IO).launch{mutex.withlock (count) { repeat(100) { count++ delay(1) } } println("count1:${count}") } val job2 = CoroutineScope(Dispatchers.IO).launch{ mutex.withLock(count) { repeat(100) { count++ delay(1) } } println("count2:${count}") } job1.join() job2.join() }Copy the code

Output: two coroutines are opened almost simultaneously to compete for the lock of count. Job1 and job2 have the same chance of getting the lock of count first

count2:100
count1:200
Copy the code

If we only execute mutex in one coroutine, it will not affect the other coroutine’s reading of count.

Be careful about using @synchronized in coroutines

Given that coroutines are only an encapsulation framework for threads, does Synchronized work with coroutines?

/** * use Synchronzied */ fun testCoroutineWithSync() = runBlocking{repeat(3){launch(Dispatchers.IO){doSomething()  } } } @Synchronized suspend fun doSomething(){ println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, start") delay(1000) println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, end") }Copy the code

We use @synchronized to mark the suspend function. Ideally, 3 coroutines should be executed in sequence, but here’s what happens:

currentThread:DefaultDispatcher-worker-1, time:1623380113511, start
currentThread:DefaultDispatcher-worker-2, time:1623380113516, start
currentThread:DefaultDispatcher-worker-3, time:1623380113516, start
currentThread:DefaultDispatcher-worker-3, time:1623380114521, end
currentThread:DefaultDispatcher-worker-2, time:1623380114521, end
currentThread:DefaultDispatcher-worker-1, time:1623380114521, end
Copy the code

The output is not exactly what we expected, and @synchronized does not act as a lock.

Even if we don’t apply the @synchronized flag, let’s change it:

suspend fun doSomething2(){ synchronized(Any()){ println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, Val time = measureTimeMillis {var count = 0 repeat(1000000){count *= count}} val time = measureTimeMillis {var count = 0 repeat(1000000){count *= count}} println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, end") } }Copy the code

The output is still the same, with no lock effect:

currentThread:DefaultDispatcher-worker-1, time:1623381184155, start
currentThread:DefaultDispatcher-worker-3, time:1623381184157, start
currentThread:DefaultDispatcher-worker-2, time:1623381184157, start
currentThread:DefaultDispatcher-worker-3, time:1623381184167, end
currentThread:DefaultDispatcher-worker-2, time:1623381184167, end
currentThread:DefaultDispatcher-worker-1, time:1623381184167, end
Copy the code

5. To summarize

Mutex is relatively easy to use in Kotlin coroutines, but it’s important to note that @synchronized is not used in suspend functions. After several days of writing, N brain a blank, the above should be not rigorous and imperfect place, welcome to add. The above code has been uploaded to Github with stars and likes