One, sharing
Data sharing is one of the main reasons for thread safety. If all the data is only valid within a thread, there is no thread-safety issue, which is one of the main reasons we often don’t need to think about thread-safety when programming. However, in multithreaded programming, data sharing is unavoidable. The most typical scenario is the data in the database. In order to ensure data consistency, we usually need to share the data in the same database. Even in the case of master and slave, the same data is accessed. Let’s now use a simple example to illustrate the problem of sharing data in multiple threads:
Code snippet 1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
package com.paddx.test.concurrent; public class ShareData { public static int count = 0 ; public static void main(String[] args) { final ShareData data = new ShareData(); for ( int i = 0 ; i < 10 ; i++) { new Thread( new Runnable() { @Override public void run() { try { // Pause for 1 ms on entry to increase the chance of concurrent problems Thread.sleep( 1 ); } catch (InterruptedException e) { e.printStackTrace(); } for ( int j = 0 ; j < 100 ; j++) {
|
The purpose of the code above is to increment count 1000 times, but this is done with 10 threads, each doing 100 times. Normally, the output should be 1000. However, if you run the above program, you will find that the result is not the same. The following is the result of an execution (the result of each run may not be the same, but sometimes the correct result may be obtained) :
It can be seen that the operation of shared variables, in the multi-threaded environment is easy to appear all kinds of unexpected results.
Two, mutual exclusion
Mutually exclusive resources are unique and exclusive, allowing only one visitor to access them at a time. We usually allow multiple threads to read data at the same time, but only one thread can write data at a time. So we usually divide locks into shared locks and exclusive locks, also known as read locks and write locks. If resources are not mutually exclusive, even if they are shared, we do not need to worry about thread safety. For example, for immutable data shares, all threads can only read them, so thread-safety issues are not a concern. However, write operations on shared data generally require mutual exclusion. In the above example, it is the lack of mutual exclusion that causes data modification problems. Java provides several mechanisms to ensure mutual exclusion, the simplest of which is Synchronized. Now add Synchronized to the program above:
Code snippet 2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
package com.paddx.test.concurrent; public class ShareData { public static int count = 0 ; public static void main(String[] args) { final ShareData data = new ShareData(); for ( int i = 0 ; i < 10 ; i++) { new Thread( new Runnable() { @Override public void run() { try { // Pause for 1 ms on entry to increase the chance of concurrent problems Thread.sleep( 1 ); } catch (InterruptedException e) { e.printStackTrace(); } for ( int j = 0 ; j < 100 ; j++) { data.addCount(); }
|
Now execute the code again and you’ll see that no matter how many times you execute it, the final result is 1000.
Atomicity
Atomicity means that operations on data are a separate, indivisible whole. In other words, an operation is a continuous, uninterrupted process in which data is not modified by other threads halfway through execution. The simplest way to guarantee atomicity is with operating system instructions, which means that if an operation corresponds to one operating system instruction, then atomicity is guaranteed. But many operations cannot be done with a single command. For example, for long operations, many systems require multiple instructions to operate on high and low levels respectively. For example, we often use the integer I ++ operation, actually need to be divided into three steps :(1) read the integer I value; (2) Add one to I; (3) Write the result back to memory. In multithreading, the following phenomena may occur:
This is why the result of code section 1 is not correct. The most common way to ensure atomicity for this combination of operations is to Lock, as Synchronized or Lock can do in Java, and Synchronized is used in section 2. In addition to locking, another method is CAS. Before modifying data, Compare And Swap (CAS). If the data is consistent with the previously read value, the data is modified; if not, the data is re-executed. However, CAS may not be valid in some scenarios, such as when another line first changes a value and then changes it back to the original value. In this case, CAS cannot be determined.
Visibility
Understanding visibility requires some understanding of the JVM’s memory model, which is similar to that of an operating system, as shown in the following figure:
We can see from the diagram, each thread has an own working memory (equivalent to a CPU, a senior buffer, the aim is to further narrowing the difference of storage system and CPU speed, improve performance), for the Shared variables, thread each read is a copy of the Shared variables in the working memory, The value of the copy in working memory is also directly modified while writing, and at some point the working memory is synchronized with the value in main memory. The problem is that if thread 1 makes changes to a variable, thread 2 May not see the changes thread 1 has made to a shared variable. We can use the following program to demonstrate the invisible problem:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
package com.paddx.test.concurrent; public class VisibilityTest { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { try { Thread.sleep( 10 ); } catch (InterruptedException e) { e.printStackTrace(); } if (! ready) { System.out.println(ready); } System.out.println(number); } } private static class WriterThread extends Thread { public void run() {
|
Intuitively, this program should only print 100; the ready value is not printed. In fact, if you execute the above code multiple times, there are many different results. Here is the result of one of the two times I run it:
When the WriterThread sets ready=true, the ReaderThread does not see the result, so it prints false. For the second result, it executes if (! When system.out.println (ready) is executed, the result of the writer thread is not read. However, it is possible that this result is due to alternate execution of threads. Visibility can be guaranteed in Java through Synchronized or Volatile, which will be discussed in a future article.
Five, sequential
To improve performance, compilers and processors may reorder instructions. There are three types of reordering:
(1) Compiler optimized reordering. The compiler can rearrange the execution order of statements without changing the semantics of a single-threaded program.
We can refer directly to the description of the reordering problem in JSR 133:
(1) (2)
First look at the source code section (1) in the figure above. From the source code, either instruction 1 or instruction 3 is executed first. If instruction 1 is executed first, R2 should not be able to see the values written in instruction 4. If instruction 3 is executed first, R1 should not be able to see the value written by instruction 2. R2 ==2, r1==1. This is the result of “reordering”. Figure (2) is a possible legal compilation result, after which the order of instructions 1 and 2 May be reversed. Therefore, r2==2, r1==1. In Java, orderliness can also be guaranteed by Synchronized or Volatile.
Six summarize
This article explains the theoretical foundations of concurrent programming in Java, some of which will be discussed in more detail in subsequent analyses, such as visibility and sequentiality. Subsequent articles will use this chapter as a theoretical basis for discussion. If you have a good understanding of the above content, I believe that whether it is to understand other concurrent programming articles or in the usual concurrent programming work, you can have a very good help.