Kotlin this is the version, and others are the same as the original version
Thread Foundation
Why use multithreading?
- Multithreading can reduce program response time. If an operation is time-consuming, you can perform it in the "background (other threads)" without making the program unable to respond.
- Compared with processes, thread creation and switching costs are less, and multithreading is very efficient in data sharing.
- Multithreading is conducive to improving CPU efficiency.
- Simplify the program structure and facilitate maintenance.
Several methods of creating threads
Inherit Thread and rewrite run method
Thread itself is also an example of implementing the Runnable interface. In this way, you only need to define a subclass of Thread, rewrite the run method to perform the required task, and then instantiate the subclass to enable threads.
object Main { @JvmStatic fun main(args: Array<String>) { MyThread().start() } class MyThread : Thread() { override fun run() { super.run() println("Frank Miles") } } }
Implement the Runnable interface and the run interface
there are two kinds. One is anonymous interface, but user-defined class implements runnable interface.
Anonymous interface:
object Main { @JvmStatic fun main(args: Array<String>) { thread { // TODO } } }
Of course, this is a simple way to write. Its source code is:
public fun thread( start: Boolean = true, isDaemon: Boolean = false, contextClassLoader: ClassLoader? = null, name: String? = null, priority: Int = -1, block: () -> Unit ): Thread { val thread = object : Thread() { public override fun run() { block() } } if (isDaemon) thread.isDaemon = true if (priority > 0) thread.priority = priority if (name != null) thread.name = name if (contextClassLoader != null) thread.contextClassLoader = contextClassLoader if (start) thread.start() return thread }
User defined classes implement interfaces, which are generally implemented in this class:
class Main : Runnable { fun runBody() { Thread(this).start() } override fun run() { } companion object { @JvmStatic fun main(args: Array<String>) { val main = Main() main.runBody() } } }
You can also define these grammatical skills outside. There is no longer a large introduction here. Readers can practice them by themselves.
Implement the Callable interface and call the method again
the Callable interface is strictly a function class in the Executor framework, which is similar to the function of Runnable, but has more powerful functions. For example:
- Callable provides a return value after the task is accepted, but Runnable does not have this function.
- Callable's call method can throw exceptions, while Runnable's run method cannot throw exceptions.
- After Callable runs, you can get the Future object; The Future object represents the result of an asynchronous operation and provides a method to check whether the operation is completed. You can use the Future object to listen to the call method called by the target thread. However, when calling the get method of Future to get the result, the current thread will be blocked until the call method of the target thread returns the result.
class Main { fun runBody() { val myCallable = MyCallable() val service = Executors.newSingleThreadExecutor() val future = service.submit(myCallable) try { println(future.get()) } catch (e: InterruptedException) { e.printStackTrace() } catch (e: ExecutionException) { e.printStackTrace() } } inner class MyCallable : Callable<String> { @Throws(Exception::class) override fun call(): String { return "Hello, Frank Miles!" } } companion object { @JvmStatic fun main(args: Array<String>) { val main = Main() main.runBody() } } }
here we don't list the role of newSingleThreadExecutor for the time being, just a macro understanding.
Summary
it is generally recommended to implement the runnbale interface. A class should be inherited only when it needs to be strengthened or modified. There is no need to rewrite the Thread method without the need to use other rewriting methods of Thread—— Design mode
Status of the thread
java thread state may be in one of the following 6 states during the life cycle of operation.
- New new creation status. The thread has been created, and start() has not been called yet. There is also a state where basic work needs to be done.
- Runnable runnable state. Once start() is called, it is in this state. The thread in this state may or may not be running, depending on the running time provided by the operating system to the thread.
- Blocked blocking status. Indicates that the thread is blocked by a lock and temporarily inactive.
- Waiting state. Temporarily inactive, do not run any code until the thread scheduler reactivates it.
- Time waiting timeout. Different from Waiting, it can return at a specified time.
- Terminated state. After the thread is executed, the code execution ends, and the exception is not caught and forcibly terminated.
after the thread is created (New), the start() call starts and enters Runnable. The following operations are shown in the figure:
Understanding of thread interruption
when a thread is running, you can call the stop() method to interrupt the thread, but this method has been abandoned. It is now recommended to use interrupt() to request thread termination.
when a thread calls this method, the thread interrupt flag bit will be set to true. The thread will detect this interrupt flag bit from time to time to determine whether the thread should be interrupted. To know whether the thread is set, you can call Thread.currentThread().isInterrupted(), as shown below:
while (Thread.currentThread().isInterrupted) { // TODO }
thread can also be called Interrupted() resets the interrupt flag bit. However, if a thread is blocked, the interrupt state cannot be detected.
if a thread is in a blocking state, if the thread detects the interrupt flag bit and it is true, it will throw an InterruptedException when the blocking method is thrown, and the interrupt flag bit will be restored to false before being thrown.
Note that interrupt() only requests termination. Whether to terminate or not depends on the thread itself.
Interrupt handling mode
If you don't know how to deal with it, here are two ways to deal with sleep as a blocking condition.
One is to reset the interrupt flag bit to true in the catch statement, and let the outside world decide whether to terminate the thread or continue through Thread.currentThread().isInterrupted():
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
the better way is to throw this problem directly, leave it to the caller (hmm?) without handling it
@Throws(InterruptedException::class) fun runBody() { Thread.sleep(100) }
Safely terminate threads
call interrupted to terminate the thread:
class Main { private var counter = 0 @Throws(InterruptedException::class) fun runBody() { val thread = Thread { while (!Thread.currentThread().isInterrupted) { counter++ println(counter) } println("stop") } thread.start() // Leave time to feel the end TimeUnit.MICROSECONDS.sleep(100) thread.interrupt() } companion object { @Throws(InterruptedException::class) @JvmStatic fun main(args: Array<String>) { val main = Main() main.runBody() } } }
you can also use @Volatile boolean to control whether the thread needs to be terminated from the main control.
class Main { @Volatile private var running = true private var counter = 0 @Throws(InterruptedException::class) fun runBody() { val thread = Thread { while (running) { counter++ println(counter) } println("stop") } thread.start() TimeUnit.MICROSECONDS.sleep(100) cancel() } private fun cancel() { running = false } companion object { @Throws(InterruptedException::class) @JvmStatic fun main(args: Array<String>) { val main = Main() main.runBody() } } }
@The volatile annotation will say later that it can be understood here that "when multiple threads are involved in accessing, modifying variables will make all threads aware as soon as possible". As soon as possible, that is, without @volatile, it doesn't matter much. It can also terminate, but when it terminates, it depends on when this thread "checks this variable".
Thread lock and synchronization
Race condition
two or more threads are reading and modifying the same variable. This behavior can be called race condition.
For example, ticket buying: the number of tickets is fixed, and there are many places to buy tickets. Ticket stations can be called threads. Ticket stations need to query and modify (buy) train tickets, which is also a competitive condition. So how to solve the problem that different passengers buy tickets for the same seat? The solution is that when a ticket station needs to buy tickets, this seat is occupied (locked), and other ticket stations can only wait (thread blocking) for the previous ticket station to buy or not, and then make a decision.
Reentry lock and condition object
ReentrantLock was introduced by java se 5.0, which is a lock that supports reentry. It means that the lock can support a thread to lock resources repeatedly. The method of use is also simple:
public class Main { public static void main(String[] args){ Main main = new Main(); main.runBody(); } private final ReentrantLock mLock = new ReentrantLock(); public void runBody() { Thread thread = new Thread(() -> { // Acquire lock mLock.lock(); // Unlock. If there is a try catch statement, it should be added in finally to ensure integrity. mLock.unlock(); }); thread.start(); } }
if an exception occurs, the lock must be released, or the subsequent thread will be permanently blocked ▲
the lock structure ensures that when accessing or modifying a value, there can only be one thread at a time, and other threads will be blocked until the system is scheduled to run.
condition objects are also called condition variables. When a thread obtains a lock and finds that the condition is not satisfied, it needs it.
I have seen many tutorials, which use transfer as an example. I think this is not intuitive enough. The purpose of using transfer example is to better explain the possible errors of deadlock and total amount change caused by multi-threaded operation, but here we mainly explain the condition object, and will not involve the above knowledge. Therefore, it is easier to understand to use codes similar to those used to apply for permission for explanation and analysis. Please look at the code first:
class Main { private val mLock = ReentrantLock() private lateinit var mCondition: Condition // Hypothetical conditions private var needRunThread = true fun runBody() { // Get condition object mCondition = mLock.newCondition() runFirst() permission } private fun runFirst() { val thread = Thread { // Acquire lock mLock.lock() try { // In real work, there may be some additional code for initiating authorization, and only output is made here. println("Initiating authorization") while (needRunThread) { println("Unauthorized, enter conditional blocking state") mCondition!!.await() } println("Authorized") } catch (pException: InterruptedException) { pException.printStackTrace() } finally { // Unlock mLock.unlock() } } thread.start() } // Let all threads blocked by this condition enter the runnable state. private val permission: Unit get() { val thread = Thread { mLock.lock() needRunThread = false println("Authorization allowed") // Let all threads blocked by this condition enter the runnable state. mCondition!!.signalAll() mLock.unlock() } thread.start() } companion object { @JvmStatic fun main(args: Array<String>) { val main = Main() main.runBody() } } }
The output of the operation is:
Initiating authorization Unauthorized, enter conditional blocking state Authorization allowed Authorized
obviously, we can explain their execution process now: first, execute runFirst() to obtain the lock, but the conditions are not satisfied, and getPermission() also has the same lock. As mentioned above, the same lock can only have one thread, which shows that runFirst() can never wait for the authorization of getPermission(), because getPermission() has been blocked by the lock. At this time, You need to conditionally block the runfirst () thread (why not end the runfirst () thread and wait for authorization before running? Because some initiation authorization preparation codes can only be executed once, and it is not easy to implement in complex scenarios.)
the conditional blocking runFirst() thread method is very simple. Calling await() can make runFirst enter the blocking state until another thread calls the signalAll() method.
- signalAll() reactivates all threads waiting for the same condition
- signal() reactivates a random thread waiting for the same condition (there is a possibility of Deadlock: the activated thread does not meet the condition, and no other thread is activated)
reactivation does not mean running threads immediately, but only relieving the blocking state of waiting threads, so that these threads can achieve object access through competition
Synchronization method block
in most cases, using ReentrantLock will be too cumbersome and duplicate too much code. Therefore, starting from java 1.0, the synchronization and conditional object mechanism embedded in the language has been introduced: every class in java has an internal lock.
for example, if a method is decorated with @Synchronized, the lock of the object where the method is located is used to protect the method. For example:
@Synchronized private fun body() {}
Equivalent to
private val mLock = ReentrantLock() private fun body() { mLock.lock() try { // todo } finally { mLock.unlock() } }
locks can also be implemented on code blocks:
synchronized (this /*Suggested Object()*/) { // todo }
here, you need to specify the Object of the lock, that is, to answer the question of whose lock. The lock of this class is used here, which is not the recommended writing method of Kotlin * * * * but Object.
In addition, the method of condition Object can be replaced by the method of Object. The replacement rules are as follows:
Condition | Object method |
---|---|
await() | wait() |
signal() / signalAll() | notify() / notifyAll() |
However, the base class of kotlin is Any, which is similar to Object in java, but does not provide wait(), notify(), notifyAll() methods. However, we can still call wait(), notify(), and notifyAll() methods by creating an instance of the Object, but note that it is synchronized(lock) rather than synchronized(this).
now, let's replace the above code without using ReentrantLock lock:
class Main { private val lock = Object() // Hypothetical conditions private var needRunThread = true fun runBody() { runFirst() permission } private fun runFirst() { val thread = Thread { try { synchronized(lock) { // In real work, there may be some additional code for initiating authorization, and only output is made here. println("Initiating authorization") while (needRunThread) { println("Unauthorized, enter conditional blocking state") lock.wait() } println("Authorized") } } catch (pException: InterruptedException) { pException.printStackTrace() } } thread.start() } // Let all threads blocked by this condition enter the runnable state. private val permission: Unit get() { val thread = Thread { synchronized(lock) { needRunThread = false println("Authorization allowed") // Let all threads blocked by this condition enter the runnable state. lock.notifyAll() } } thread.start() } companion object { @JvmStatic fun main(args: Array<String>) { val main = Main() main.runBody() } } }
Summary
synchronous code is non fragile, difficult to correct, and difficult to maintain. It is usually used only in simple threads. Generally, java.util Classes under the concurrent package. Generally speaking, it is recommended to use Lock/Condition structure only when it is particularly needed; It is recommended to use synchronized code blocks only when the synchronization method is suitable for your program.
Memory model and concurrency
After compilation, kotlin is the same as java, which is of class type. Therefore, the bottom layer is the same.
java Memory Model
Java heap memory is used to store object instances. Heap memory is a memory area shared by all threads, while local variables and method defined parameters are not shared in threads.
java memory defines the relationship between threads and main memory: shared variables between threads are stored in main memory, but threads hold a private copy. When a thread copy is modified, the thread will submit data to main memory at the appropriate time, and then other threads update the value of the copy from main memory at the appropriate time.
it should be noted that this is only an abstract concept, which does not really exist. It is explained from the perspective of cache, write buffer and deposit area. The Java memory model controls the communication between threads, which determines when the writing of shared variables in main memory by one thread is visible to another thread. The schematic diagram can be shown as follows:
thread A communicates with thread B: the variable data updated by thread A is submitted to main memory, and then thread B reads the main memory variables at an appropriate time and updates them to the copy.
Concurrency: atomicity, visibility, reordering, and ordering
Atomicity
the reading and assignment of basic data are atomic operations, which cannot be interrupted, either executed or not executed. For example:
x = 3;
in particular, the following operations are not atomic:
y = x; x++;
y = x, two instructions are executed: 1. Read x, 2. Assign the value of X to y, so it is not atomic.
X++, the auto increment operation is not atomic, because it is equivalent to x = x +1. It naturally performs three steps: 1. Read x, 2. Add 1 to the value x, 3. Assign the value to X. Therefore, it is not atomic.
java.util.concurrent. Many classes under the atomic package use machine level instructions (not locks) to ensure the atomicity of the given algorithm, such as AtomicIntenger's methods. These methods are not introduced. Please check yourself. It is worth noting that these classes are only used by system programmers who develop concurrency tools, and application programmers should not use these classes.
visibility
visibility refers to the visibility between threads. The modified state of one thread is visible to another thread. The modified result of one thread can be immediately known by another thread. Variables modified by volatile can ensure that the value is immediately updated to main memory, and all other threads can see and read the new value. Variables that are not modified by volatile may not be immediately updated to main memory. Naturally, other threads may read old values.
Reorder
a means by which the compiler reorders and executes instructions for or run-time environment to optimize the program. It is divided into compile time sorting generated by compile time environment and run-time sorting generated by run-time environment.
Orderliness
the Java memory model allows the compiler and processor to reorder instructions. Reordering will not have any impact on the correctness of single thread code execution, but will affect the correctness of multi-threaded concurrent execution., In addition to volatile, the lock mentioned above can also be used to ensure orderliness, that is, multi-threaded operation instructions are passively sorted by locks to achieve sequential access, rather than cross access and value modification.
@Volatile usage
@The variable modified by volatile will automatically add volatile statements after compilation, which is the same as java, but the writing method is different.
@Volatile is designed to provide a lock free mechanism for synchronous access by solving the simple reading and writing of oneortwo instances. A domain decorated with @Volatile, so the compiler and virtual machine know that this domain may be updated concurrently by another thread.
after the shared variable is modified by @Volatile, it will have two meanings. 1, After a thread modifies a variable, other threads can immediately perceive it. 2, Reordering with instructions is prohibited.
Atomicity is not guaranteed: @Volatile does not say that all operations involving this variable are atomicity: for example, autoincrement does not belong to. In fact, there is really no atomicity, just to ensure that when data is modified, other threads can immediately perceive it.
Ensure orderliness: because @Volatile can prohibit instruction reordering. It can be explained that the statement before the @Volatile variable will not be executed after @Volatile.
@Volatile may be better than synchronized, but better than not meeting atomicity, so it cannot replace synchronized.
generally speaking, the following conditions must be met to use @Volatile:
-
Writes to variables do not depend on the current value. That is, assignments such as self increment and self decrement cannot rely on the original variable operation.
-
This variable is not contained in an invariant with other variables. In other words, if multiple variables modified by @Volatile must meet some invariant formulas (for example, @Volatile a must be greater than @Volatile b), this invariant is at risk of being broken due to the operation of multiple threads.
There are generally two ways to use @Volatile, and you can consult the data for other specific uses.
- The status flag is very common, and we have also used it:
class Main { @Volatile private var running = true private var counter = 0 @Throws(InterruptedException::class) fun runBody() { val thread = Thread { while (running) { counter++ println(counter) } println("stop") } thread.start() TimeUnit.MICROSECONDS.sleep(100) cancel() } private fun cancel() { running = false } companion object { @Throws(InterruptedException::class) @JvmStatic fun main(args: Array<String>) { val main = Main() main.runBody() } } }
- DCL (double check mode)
Double check mode is generally used in the singleton mode of design mode.
class Main private constructor() { companion object { @Volatile private var instance: Main? = null // A little performance is sacrificed here to ensure correctness fun getInstance(): Main { // Prevent redundant synchronization if (instance == null) { synchronized(Main::class.java) { // Prevent redundant instantiation if (instance == null) { instance = frms.Main() } } } return instance!! } } }
Summary
@Volatile is a simple and fragile synchronization mechanism, which is indeed superior to the performance of locks in simple cases. Strictly abiding by the use rules can indeed simplify the code, but because the code with @Volatile errors is more difficult to find and maintain than synchronized, and it is also more prone to errors, please use it as appropriate.