Multithreading series:

Multithreading (1) — The basic concept of threads

Introduction of 0.

There are many unexpected phenomena that can occur in multithreading. To understand the causes of these phenomena, it is important to understand the following concepts.

1. Java thread memory model

The Java memory model mainly defines the access rules for variables. Variables here only refer to instance variables, static variables, not local variables, because local variables are thread private and do not share. There are several main elements in this model:

  • thread
  • Shared variables
  • The working memory
  • Main memory

There are a few other things to note between these elements:

Role in instructions
The thread itself Each thread has its own working memory, which contains copies of shared variables.
Threads operate on shared variables Threads can only operate on a copy of a shared variable in their own working memory, not on a shared variable in main memory.
Variables are shared between operations on different threads Different threads cannot directly manipulate variables in each other’s working memory, only through the main thread.

Here is a diagram of the relationship between these elements:

1.1 Inter-Memory Operations

Java defines eight operations to manipulate variables. These eight operations are defined as follows:

operation Role in instructions
Lock up Main memory variable Identifies a variable as a thread-exclusive state
UNLOCK Main memory variable Release a locked variable before it can be locked by another thread
Read (read) Main memory variable Transfer the value of a variable from main memory to the thread’s working memory for subsequent load action
Load Working memory variable Put the variables from the read operation into a copy of the variables in working memory
Use STH. Working memory variable Passes the value of a variable in working memory to the execution engine
Assign Working memory variable Assigns the value received by the execution engine to a variable in working memory
Store Working memory variable Pass the value of a variable in working memory to main memory for subsequent write operations
Write (write) Main memory variable Puts the value of the variable obtained from working memory by the store operation into the main memory variable

1.1.1 Memory Operation Rules

Java memory model operations must also meet the following rules:

Operation method The rules
Read and the load The two methods must be combined to not allow a variable to be read from main memory but not accepted by working memory
The store and write The two methods must be combined to not allow a storage operation to be initiated from working memory but not accepted by main memory
assign Working memory variables that are not assigned are not allowed to be synchronized to main memory
The load and use Before the use operation, the load operation must be performed
The assign and store Before the store operation, the assign operation must be performed
The lock and unlock 1. Unlock can only apply to a variable locked by a LOCK operation

2. A variable must be unlocked as many times as it has been locked
lock 1. A variable can only be locked by one thread at a time

2. After the LOCK operation is performed, the variable values of the working memory will be cleared. Therefore, you need to load or assign the variable values again
unlock Before an unlock operation can be performed on a variable, the variable must be synchronized back to main memory

Don’t write these down, just check back when you use them.

2. Several important concepts in multithreading

Once you understand Java’s memory model, you need to continue to understand several important concepts that help you understand the multithreading phenomenon.

2.1 Synchronous and Asynchronous

Both synchronous and asynchronous describe a method call. Their concepts are as follows:

  • Synchronization: The caller must wait until the called method returns before continuing with the subsequent behavior.

  • Asynchrony: After a call, the caller can continue the subsequent behavior without waiting for the calling method to return.

The following two graphs clearly illustrate the difference between synchronous and asynchronous:

2.2 Concurrency and parallelism

Concurrency and parallelism describe the state of multiple tasks. They are defined as follows:

  • Concurrency: Multiple tasks run alternately.

  • Parallelism: Multiple tasks run simultaneously.

In fact, the difference between these two concepts is that one is alternation and the other is simultaneity. In fact, if there is only one CPU, the system can not execute tasks in parallel, only concurrent, because the CPU can only execute one instruction at a time. So if you want to achieve parallelism, you need multiple cpus. To illustrate these two concepts, take a look at the following two diagrams:

2.3 atomic

Atoms are indivisible particles in chemical reactions. So the concept of atomicity is as follows:

Atomicity: In Java, it refers to operations that are indivisible.

For example, the memory operations just described are all atomic operations. Here’s another example to help you understand:

x = 1;
y = x;
Copy the code

Which of these two lines of code is an atomic operation and which is not? X = 1 is, because the thread is writing the number 1 directly to the working memory. Y = x is not, because there are two operations involved:

  1. Read the value of x (because x is a variable)
  2. Writes the value of x to working memory

2.4 the visibility

Visibility: When one thread changes the value of a shared variable, other threads are immediately aware of the change.

To illustrate the importance of this visibility, here’s an example:

public class ThreadTest {
	
	
	private static boolean plus = true;
	private static int a;
	
	static class VisibilityThread1 extends Thread {
			
		
		public VisibilityThread1(String name) {
			setName(name);
		}
		
		@Override
		public void run() {
			while(true) {
				if(plus) {
					a++;
					plus = false;
					System.out.println(getName() + " a = " + a + " plus = " + plus);
				}
			}
		}
		
	}

	static class VisibilityThread2 extends Thread {
		
		public VisibilityThread2(String name) {
			setName(name);
		}
		
		@Override
		public void run() {
			while(true) {
				if(! plus) { a--; plus =true;
					System.out.println(getName() + " a = " + a + " plus = " + plus);
				}
			}

		}
		
	}
	
	
	public static void main(String[] args) {
		
		VisibilityThread1 visibilityThread1 = new VisibilityThread1(Thread 1 "");
		VisibilityThread2 visibilityThread2 = new VisibilityThread2(Thread 2 ""); visibilityThread1.start(); visibilityThread2.start(); }}Copy the code

The expected output from this code should be the following two loops:

Thread 1 a = 1 plus =falseThread 2 a = 0 plus =true
Copy the code

But here’s what you’ll find:

Thread 1 a = 0 plus =trueThread 2 a = 1 plus =false
Copy the code

This error occurs because both threads are modifying the shared variables A and plus at the same time. When one thread modiates a shared variable, other threads do not know that the shared variable has been modified, so it is important to pay attention to visibility in multi-threaded development.

2.5 reorder

Reordering: A means by which compilers and processors reorder instructions to optimize program performance. Before I explain this concept, I want to introduce the concept of data dependency.

2.5.1 Data dependency

If two operations operate on a variable at the same time, and one of the operations also includes writes, then there is a data dependency between the two operations. See the table below for these combined operations:

The name of the instructions Code sample
Writing after reading Write a variable and then read the variable a = 1;

b = a;
Write after Write a variable, and then write that variable a = 1;

a = 2;
Got to write After reading a variable, write the variable b = a;

a = 2;

These are the three cases where reordering changes the result of the program. So the compiler and processor do not reorder these data-dependent operations. Note that data dependencies are only available for single-threaded applications. If multithreaded, the compiler and processor do not have data dependencies.

2.5.2 Reordering in multithreading

Here we use simplified code, as follows:

int a = 0;
boolean flag = false; VisibilityThread1 {a = 3; // 1 flag =true; // thread 2 VisibilityThread2 {if(flag) { // 3 a= a * 3; / / 4}}Copy the code

There are no data dependencies for operations 1,2 and 3,4, so it is possible for the compiler and processor to reorder these combinations of operations. One of the situations in which the program is executed is shown below:

Because operations 5 and 6 in thread 2 have a control-dependent relationship, which affects the speed of the program, the compiler and processor can guess which way to execute to speed up the program. In this case, thread 2 reads the value of A ahead of time. The value of A * 3 is calculated and temporarily saved in the hardware cache of the resort buffer. After the value of Flag becomes true, the stored value is written into A. But that would be something that we don’t want, in which case, a might still be 1.

2.6 order

The idea of orderliness is actually pretty easy to understand if you understand reordering. Orderliness: The order in which the program is run is the same as the order in which the code was written.

3. Thread safety

Once you understand the concepts above, it may be easier to understand the concept of thread safety.

3.1 define

Thread safety means that when a method is called in a multi-threaded environment, it can correctly handle the shared variables between multiple threads, so that the program function can be correctly executed. Here’s a classic thread-safety example — multi-window tickets. Let’s say there are 30 tickets, and now there are two Windows that sell those 30 tickets at the same time. Tickets are shared variables and Windows are threads. The code logic here can be roughly divided into these steps:

  1. Each time a ticket is sold, the total number of tickets is subtracted by one.
  2. If the total number of votes is 0, stop the loop.

The code is as follows:

public class SellTicketDemo implements Runnable {

	private int ticketNum = 30;
	
	@Override
	public void run() {
		while(true) {
			
			if(ticketNum <= 0) {
				break;
			}
			
			System.out.println(Thread.currentThread().getName() +"Sell order" + ticketNum + "One ticket, remaining number of votes:" + --ticketNum);
		}
	}
	
	public static void main(String[] args) {
		
		SellTicketDemo sellTicketDemo = new SellTicketDemo();
		
		Thread thread1 = new Thread(sellTicketDemo,"Window 1");
		Thread thread2 = new Thread(sellTicketDemo,"Window 2"); thread1.start(); thread2.start(); }}Copy the code

The code is printed as follows:

Window 1 sells 30th tickets, remaining number of votes: 28 Window 2 sells 30th tickets, remaining number of votes: 29 window 1 sells 28th tickets, remaining number of votes: 27 window 2 sells 27th tickets, remaining number of votes: 26 window 1 sells 26th tickets, remaining number of votes: 25 window 2 sells the 25th ticket, remaining votes: 24 window 1 sells the 24th ticket, remaining votes: 23 window 2 sells the 23rd ticket, remaining votes: 22 window 2 sells the 21st ticket, remaining votes: 20 window 1 sells the 22nd ticket, remaining votes: 21 window 2 sells the 20th ticket, remaining votes: 19 window 1 sells the 19th ticket, remaining votes: 18 window 1 sells the 17th ticket, remaining votes: 16 window 1 sells the 16th ticket, remaining votes: 15 window 1 sells the 15th ticket, remaining votes: 14 window 1 sells the 14th ticket, remaining votes: 13 window 1 sells the 13th ticket, remaining votes: 12 window 1 sells the 12th ticket, remaining votes: 11 window 1 sells the 11th ticket, remaining votes: 10 window 1 sells the 10th ticket, remaining votes: 9 window 1 sold the 9th ticket, remaining number of votes: 8 window 1 sold the 8th ticket, remaining number of votes: 7 window 1 sold the 7th ticket, remaining number of votes: 6 window 1 sold the 6th ticket, remaining number of votes: 5 window 1 sold the 5th ticket, remaining number: 4 Window 1 sells the fourth ticket, remaining number of votes: 3 Window 1 sells the third ticket, remaining number of votes: 2 window 1 sells the second ticket, remaining number of votes: 1 window 1 sells the first ticket, remaining number of votes: 0 window 2 sells the 18th ticket, remaining number of votes: 17Copy the code

As can be seen from the print result above, both window 1 and window 2 sell the 30th ticket at the same time, which is not consistent with what we expect, and this is the thread is not safe.

4. Synchronized modifier

So how can the ticketing case be thread safe? One way to do this is with synchronized.

4.1 Synchronized code block

4.1.1 Syntax Format

Synchronized (obj) {// synchronized code block}Copy the code

4.1.2 Using synchronized code blocks

The obj of synchronized parentheses is a synchronization monitor, and Java allows any object to be used as a synchronization monitor; SellTicketDemo instances are used as synchronization monitors. The code is as follows:

public class SellTicketDemo implements Runnable {

	private int ticketNum = 30;
	
	@Override
	public void run() {
		while(true) {
			synchronized(this) {
				if(ticketNum <= 0) {
					break;
				}
				
				System.out.println(Thread.currentThread().getName() +"Sell order" + ticketNum + "One ticket, remaining number of votes:" + --ticketNum);
			}
		}
	}
	
	public static void main(String[] args) {
		
		SellTicketDemo sellTicketDemo = new SellTicketDemo();
		
		Thread thread1 = new Thread(sellTicketDemo,"Window 1");
		Thread thread2 = new Thread(sellTicketDemo,"Window 2"); thread1.start(); thread2.start(); }}Copy the code

The print result is as follows:

Window 1 sells the 30th ticket, remaining number of votes: 29 Window 1 sells the 29th ticket, remaining number of votes: 28 window 1 sells the 28th ticket, remaining number of votes: 27 window 1 sells the 27th ticket, remaining number of votes: 26 window 1 sells the 26th ticket, remaining number of votes: 25 window 1 sells the 25th ticket, remaining votes: 24 window 1 sells the 24th ticket, remaining votes: 23 window 1 sells the 23rd ticket, remaining votes: 22 window 1 sells the 22nd ticket, remaining votes: 21 window 1 sells the 21st ticket, remaining votes: 20 window 2 sells the 20th ticket, remaining number of votes: 19 window 2 sells the 19th ticket, remaining number of votes: 18 window 2 sells the 18th ticket, remaining number of votes: 17 window 2 sells the 17th ticket, remaining number of votes: 16 window 2 sells the 16th ticket, remaining number of votes: 15 window 2 sells the 15th ticket, remaining votes: 14 window 2 sells the 14th ticket, remaining votes: 13 window 2 sells the 13th ticket, remaining votes: 12 window 2 sells the 12th ticket, remaining votes: 11 window 2 sells the 11th ticket, remaining votes: 10 window 2 sold the 10th ticket, remaining number of votes: 9 window 2 sold the 9th ticket, remaining number of votes: 8 window 2 sold the 8th ticket, remaining number of votes: 7 window 2 sold the 7th ticket, remaining number of votes: 6 window 2 sold the 6th ticket, remaining number: 5 Window 2 sells the fifth ticket, the remaining number of votes: 4 window 2 sells the fourth ticket, the remaining number of votes: 3 Window 2 sells the third ticket, the remaining number of votes: 2 window 2 sells the second ticket, the remaining number of votes: 1 window 2 sells the first ticket, the remaining number of votes: 0Copy the code

And you can see that this is now the correct answer.

4.2 a synchronized method

4.2.1 Syntax format

Synchronized [return value] [method name](parameter...) {}Copy the code

4.2.2 Use the synchronized method

Use synchronized to modify the multithreaded operation method is very simple, the code is as follows:

public class SellTicketDemo implements Runnable {

	private int ticketNum = 30;
	
	@Override
	public void run() {
		while(true) {

			sellTicket();
			
		}
	}
	
	public synchronized void sellTicket() {
		if(ticketNum <= 0) {
			return;
		}
		
		System.out.println(Thread.currentThread().getName() +"Sell order" + ticketNum + "One ticket, remaining number of votes:" + --ticketNum);
	}
	
	public static void main(String[] args) {
		
		SellTicketDemo sellTicketDemo = new SellTicketDemo();
		
		Thread thread1 = new Thread(sellTicketDemo,"Window 1");
		Thread thread2 = new Thread(sellTicketDemo,"Window 2"); thread1.start(); thread2.start(); }}Copy the code

Print the following:

Window 1 sells the 30th ticket, remaining number of votes: 29 Window 1 sells the 29th ticket, remaining number of votes: 28 window 1 sells the 28th ticket, remaining number of votes: 27 window 1 sells the 27th ticket, remaining number of votes: 26 window 1 sells the 26th ticket, remaining number of votes: 25 window 1 sells the 25th ticket, remaining votes: 24 window 1 sells the 24th ticket, remaining votes: 23 window 1 sells the 23rd ticket, remaining votes: 22 window 1 sells the 22nd ticket, remaining votes: 21 window 1 sells the 21st ticket, remaining votes: 20 window 1 sells the 20th ticket, remaining number of votes: 19 window 2 sells the 19th ticket, remaining number of votes: 18 window 2 sells the 18th ticket, remaining number of votes: 17 window 2 sells the 17th ticket, remaining number of votes: 16 window 2 sells the 16th ticket, remaining number of votes: 15 window 2 sells the 15th ticket, remaining votes: 14 window 2 sells the 14th ticket, remaining votes: 13 window 2 sells the 13th ticket, remaining votes: 12 window 2 sells the 12th ticket, remaining votes: 11 window 2 sells the 11th ticket, remaining votes: 10 window 2 sold the 10th ticket, remaining number of votes: 9 window 2 sold the 9th ticket, remaining number of votes: 8 window 2 sold the 8th ticket, remaining number of votes: 7 window 2 sold the 7th ticket, remaining number of votes: 6 window 2 sold the 6th ticket, remaining number: 5 Window 2 sells the fifth ticket, the remaining number of votes: 4 window 2 sells the fourth ticket, the remaining number of votes: 3 Window 2 sells the third ticket, the remaining number of votes: 2 window 2 sells the second ticket, the remaining number of votes: 1 window 2 sells the first ticket, the remaining number of votes: 0Copy the code

Reference articles and books:

Java concurrency atomicity, visibility, order

Research on Java memory access reordering

The art of Concurrent programming in Java

Java concurrent programming practice

Practical Java high concurrency programming

In-depth understanding of the Java virtual machine