Copyright belongs to the author, any form of reprint please contact the author to obtain authorization and indicate the source.

What is thread safety?

Why is there a thread safety issue?

When multiple threads are writing to the same global or static variable at the same time, data collisions can occur, which is a thread-safety problem. However, data conflicts do not occur when performing read operations.

Case: The demand now has 100 train tickets, there are two Windows snatching train tickets at the same time, please use multi-thread simulation of snatching tickets effect.

public class ThreadTrain implements Runnable {
    private int trainCount = 10;

    @Override
    public void run() {
        while (trainCount > 0) {
            try {
                Thread.sleep(500);
                sale();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void sale() {
        if (trainCount > 0) {
            --trainCount;
            System.out.println(Thread.currentThread().getName() + ", sell the first" + (10 - trainCount) + "Ticket");
        }
    }

    public static void main(String[] args) {
        ThreadTrain threadTrain = new ThreadTrain();
        Thread t1 = new Thread(threadTrain, "1");
        Thread t2 = new Thread(threadTrain, "Two"); t1.start(); t2.start(); }}Copy the code

Running results:

The 99th train ticket will be sold at window 1 and Window 2 at the same time, and some train tickets will be on repeat sale. It is found that data conflict may occur when multiple threads share the same global member variable.

Thread-safe workaround:

Q: How to solve the problem of thread safety between multiple threads A: use synchronized or lock between multiple threads.

Q: Why would using thread synchronization or locking solve thread-safety problems? A: There will be data collision issues (thread insecurity issues) and only one thread will be allowed to execute. Release the lock after code execution is complete, allowing other threads to execute. This solves the thread insecurity problem.

Q: What is inter-threaded synchronization? A: When multiple threads share the same resource, there is no interference from other threads.

Q: What is multi-threaded synchronization? A: When multiple threads share the same resource, there is no interference from other threads.

The built-in lock

Java provides a built-in locking mechanism to support atomicity. Each Java object can be used as a synchronized lock, called a built-in lock. The lock is automatically acquired by a thread before it enters a synchronized code block and released when the block exits normally or when an exception is thrown in the block

The built-in lock is mutually exclusive. That is, after thread A obtains the lock, thread B blocks until thread A releases the lock, and thread B can obtain the same lock. The built-in lock is implemented using the synchronized keyword, which can be used in two ways:

  1. Modifies the method that needs to be synchronized (all methods that access state variables must be synchronized), in which case the object that acts as the lock is the object that called the synchronized method
  2. Synchronizing a block of code is the same as using the synchronized modifier directly, but the granularity of the lock can be finer, and the object that acts as the lock may not be this, but other objects, making it more flexible to use

Synchronized code block

That is to include code that can have thread safety problems. Synchronized {thread conflict can occur} is the code block synchronized(object)// this object can be any object {code to be synchronized}Copy the code

The object is like a lock. The thread holding the lock can execute in synchronization. The thread without the lock cannot enter even if it has the execution right of the CPU.

  1. There must be two or more threads
  2. Multiple threads must use the same lock

It is necessary to ensure that only one thread is running in the synchronization. Advantage: It solves the safety problem of multithreading. Disadvantage: multiple threads need to judge the lock, which consumes resources and grabs the lock resources. Example code:

private void sale() {
        synchronized (this) {
            if (trainCount > 0) {
                --trainCount;
                System.out.println(Thread.currentThread().getName() + ", sell the first" + (10 - trainCount) + "Ticket"); }}}Copy the code

Synchronized methods

Methods that modify synchronized are called synchronized methods

public synchronized void sale() {
	if (trainCount > 0) {
		System.out.println(Thread.currentThread().getName() + ", sell the first" + (100 - trainCount + 1) + "Ticket"); trainCount--; }}Copy the code

What lock does the synchronous method use?

A: Synchronization functions use this lock. Proof: One thread uses the synchronization block (this open lock) and the other thread uses the synchronization function. If the two threads cannot synchronize, a data error will occur. Reference: method lock, object lock and class lock usage and differences

package com.itmayiedu;

class Thread0009 implements Runnable {
    private int trainCount = 10;
    private Object oj = new Object();
    public boolean flag = true;

    public void run() {

        if (flag) {
            while (trainCount > 0) {
                synchronized (this) {
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
                        // TODO: handle exception
                    }
                    if (trainCount > 0) {
                        System.out.println(Thread.currentThread().getName() + "," + "Sale clause" + (10 - trainCount + 1) + "Ticket"); trainCount--; }}}}else {
            while (trainCount > 0) {
                sale();
            }

        }

    }

    public synchronized void sale() {
        try {
            Thread.sleep(10);
        } catch (Exception e) {
            // TODO: handle exception
        }
        if (trainCount > 0) {
            System.out.println(Thread.currentThread().getName() + "," + "Sale clause" + (10 - trainCount + 1) + "Ticket");
            trainCount--;
        }

    }
}

public class Test009 {
    public static void main(String[] args) throws InterruptedException {
        Thread0009 threadTrain1 = new Thread0009();
        Thread0009 threadTrain2 = new Thread0009();
        threadTrain2.flag = false;

        Thread t1 = new Thread(threadTrain1, "Window 1");
        Thread t2 = new Thread(threadTrain2, "Window 2"); t1.start(); Thread.sleep(40); t2.start(); }}Copy the code

Static synchronization function

Add the static keyword to the method, use the synchronized keyword, or use a.class file. The lock used by a static synchronization function is the bytecode file object to which the function belongs, which can be obtained using the getClass method or represented by the current class name. Class. Example code:

public static void sale() {
		synchronized (ThreadTrain3.class) {
			if (trainCount > 0) {
				System.out.println(Thread.currentThread().getName() + ", sell the first" + (100 - trainCount + 1) + "Ticket"); trainCount--; }}}Copy the code

Summary: Synchronized modifiers use the current this lock. Synchronized modifiers static methods that use locks are the bytecode files of the current class

Multithreaded deadlock

Synchronization is nested, so the lock cannot be released

public class ThreadTrain3 implements Runnable {
	private int trainCount = 100;
	private boolean flag = true;

	@Override
	public void run() {
	    if(flag){
    		while (true) {// If flag istrueSynchronized (threadtrain3. class){sale(); }}}else{// If flag isfalseGet this lock, then obj lock to executewhile(true){
		        sale();
		    }
		}
	}

	public synchronized void sale() {
		synchronized (ThreadTrain3.class) {
			if (trainCount > 0) {
			    try{
			        Thread.sleep(40);
			    }catch(Exception e){}
				System.out.println(Thread.currentThread().getName() + ", sell the first" + (100 - trainCount + 1) + "Ticket");
				trainCount--;
			}
		}

	}
	
	public static void main(String[] args) {
		ThreadTrain3 threadTrain = new ThreadTrain3();
		Thread t1 = new Thread(threadTrain, "Number 1");
		Thread t2 = new Thread(threadTrain, "(2) no.");
		t1.start();
		Thread.sleep(40);
		threadTrain.flag = false; t2.start(); }}Copy the code

What is a Threadlocal

ThreadLocal raises the local variables of a thread, which has its own local variables for accessing a thread. When using ThreadLocal to maintain variables, ThreadLocal provides a separate copy of the variable for each thread that uses the variable, so each thread can independently change its own copy without affecting the corresponding copy of other threads. The ThreadLocal interface is simple, with only four methods:

  • Void set(Object value) Sets the value of thread-local variables for the current thread.
  • Public Object get() This method returns the thread-local variable corresponding to the current thread.
  • Public void remove() removes the value of a thread-local variable. This method is new in JDK 5.0 to reduce memory usage. It is important to note that local variables to the thread are automatically garbage collected when the thread terminates, so it is not necessary to explicitly call this method to clean up local variables of the thread, but it can speed up memory collection.
  • Protected Object initialValue() returns the initialValue of the thread-local variable. This method is a protected method, obviously designed to be overridden by subclasses. This method is a deferred call that is executed only once, the first time the thread calls GET () or set(Object). The default implementation in ThreadLocal simply returns null.

Example: Create three threads, each generating its own independent serial number.

class Res {
	public static Integer count = 0;
	public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
		protected Integer initialValue() {
			return 0;
		};
	};

	public Integer getNum() {
		int count = threadLocal.get() + 1;
		threadLocal.set(count);
		return count;
	}
}

public class Test006 extends Thread {

	private Res res;

	public Test006(Res res) {
		this.res = res;
	}

	@Override
	public void run() {
		for (int i = 0; i < 3; i++) {
			System.out.println(Thread.currentThread().getName() + ","+ res.getNum()); } } public static void main(String[] args) { Res res = new Res(); Test006 t1 = new Test006(res); Test006 t2 = new Test006(res); t1.start(); t2.start(); }}Copy the code

ThreadLoca uses a set of maps, map.put (” current thread “, value);

Multithreading has three main features

What is atomicity

That is, one or more operations are either all performed without interruption by any factor, or none at all. A classic example is the bank account transfer problem: for example, transferring 1000 yuan from account A to account B must involve two operations: subtract 1000 yuan from account A and add 1000 yuan to account B. These two operations must be atomic in order to avoid unexpected problems. The same is true when we manipulate data, such as I = I +1; And that includes reading I, calculating I, writing I. This line of code is not atomic in Java, so multithreading would be problematic, so we need to use synchronization and locking to ensure this feature. Atomicity is part of keeping data consistent and thread safe,

What is visibility

When multiple threads access the same variable, one thread changes the value of the variable, and the other threads immediately see the changed value. If two threads are on different cpus, and thread 1 changes the value of I before flushing it to main memory, and thread 2 uses it again, then the value of I must be the same as before, and thread 1 doesn’t see the changes to the variable and that’s a visibility problem.

What is order

The order in which the program is executed is the order in which the code is executed. Generally speaking, in order to improve the efficiency of the program, the processor may optimize the input code. It does not ensure that the execution sequence of each statement in the program is the same as that in the code, but it will ensure that the final execution result of the program is the same as the result of the code execution sequence. As follows:

int a = 10; // statement 1 int r = 2; // statement 2 a = a + 3; // statement 3 r = a*a; / / 4Copy the code

Because of reordering, he might also execute 2-1-3-4, 1-3-2-4, but never 2-1-4-3, because that breaks the dependency. Obviously reordering is not a problem with single-threaded execution, but multithreaded execution is not, so we need to take this into account when programming with multiple threads.

Java memory model

The shared Memory model refers to the Java Memory model (JMM), which determines that a shared variable written by one thread can be visible to another thread. From an abstract point of view, JMM defines an abstract relationship between threads and mainmemory: shared variables between threads are stored in mainmemory, and each thread has a private local memory where it stores copies of shared variables to read/write. Local memory is an abstraction of the JMM and does not really exist. It covers caches, write buffers, registers, and other hardware and compiler optimizations.

  1. First, thread A flusher the updated shared variables from local memory A to main memory.
  2. Thread B then goes into main memory to read the shared variables that thread A has updated previously.

The following is a schematic illustration of these two steps:

Taken as A whole, these two steps are essentially thread A sending messages to thread B, and this communication must go through main memory. The JMM provides Java programmers with memory visibility assurance by controlling the interaction between main memory and local memory for each thread.

Summary: What is the Java Memory Model? The Java Memory Model, or JMM, defines the visibility of one thread to another. Shared variables are stored in main memory, and each thread has its own local memory. When multiple threads access the same data at the same time, local memory may not be flushed to main memory in time, so thread-safety issues can occur.

Volatile

Visibility means that if a thread modifies a volatile variable, it guarantees that the value is immediately updated to main memory, and that the value can be immediately retrieved when another thread needs to read it. In Java, to speed up the program, operations on variables are performed on the thread’s register or CPU cache before being synchronized to main memory. Volatile variables are read from and written to main memory.

Volatile guarantees immediate visibility of shared variables between threads, but does not guarantee atomicity

class ThreadDemo004 extends Thread {
    public boolean flag = true;

    @Override
    public void run() {
        System.out.println("Thread started...");
        while (flag) {

        }
        System.out.println("Thread terminated...");
    }

    public void setRuning(boolean flag) {
        this.flag = flag;
    }
}

public class Test0004 {
    public static void main(String[] args) throws InterruptedException {
        ThreadDemo004 threadDemo004 = new ThreadDemo004();
        threadDemo004.start();
        Thread.sleep(3000);
        threadDemo004.setRuning(false);
        System.out.println("Flag has been changed to false");
        Thread.sleep(1000);
        System.out.println("flag:"+ threadDemo004.flag); }}Copy the code

The result has been set to Fasle why? It’s still running. Cause: Threads are not visible to each other, copies are being read, and the main memory result is not being read in time. The solution is to use the Volatile keyword to address interthread visibility, forcing threads to go to “main memory” each time they read the value

Volatile features

  1. This ensures that the variable is visible to all threads. This “visibility”, as described at the beginning of this article, ensures that when a thread changes the value of the variable, the new value is immediately synchronized to main memory and flushed from main memory immediately before each use. This is not the case with ordinary variables, whose values are passed between threads through main memory (see The Java Memory Model).
  2. Disallow instruction reordering optimization. For volatile variables, the “load addL $0x0, (%esp)” operation is performed after the assignment. This operation acts as a memory barrier, which is not required when only one CPU accesses memory. Instruction reordering: the CPU adopts a system that allows multiple instructions to be distributed to each circuit unit in an unprogrammed order.

Volatile performance: Volatile has almost the same read performance cost as normal variables, but writes are slower because it requires inserting many memory-barrier instructions into native code to keep the processor from executing out of order.

The difference between Volatile and Synchronized

  1. Thus we can see that volatile, while visible, does not guarantee atomicity.
  2. In terms of performance, synchronized prevents multiple threads from executing a piece of code at the same time, which will affect the efficiency of program execution, while volatile keyword is better than synchronized in some cases.

Note, however, that volatile is no substitute for synchronized because volatile does not guarantee atomicity.

reorder

Data dependency

If two operations access the same variable, and one of them is a write operation, there is a data dependency between the two operations. There are three types of data dependencies:

The name of the Code sample instructions
Writing after reading a = 1; b = a; After writing a variable, read the position.
Write after a = 1; a = 2; You write a variable, and then you write that variable.
Got to write a = b; b = 1; After reading a variable, write the variable.

In the above three cases, the result of the program’s execution will be changed by reordering the execution order of the two operations. As mentioned earlier, the compiler and processor may reorder operations. The compiler and processor adhere to data dependencies when reordering, and do not change the order in which two operations with data dependencies are executed. Note that data dependencies are only for sequences of instructions executed in a single processor and operations performed in a single thread. Data dependencies between different processors and between different threads are not considered by compilers and processors.

The as – if – serial semantics

The result of a (single-threaded) program cannot be changed, no matter how much it is reordered (to improve parallelism by the compiler and processor). The compiler, runtime, and processor must comply with the AS-IF-Serial semantics.

To comply with the as-if-serial semantics, the compiler and processor do not reorder operations that have data dependencies because such reordering changes the execution result. However, if there are no data dependencies between the operations, they may be reordered by the compiler and processor. To illustrate, look at the following code example for calculating the area of a circle:

Double PI = 3.14; //A double r = 1.0; //B double area = pi * r * r; //CCopy the code

The data dependencies of the above three operations are shown below:

Procedural order rule

The sample code above that calculates the area of a circle has three happens-before relationships according to the happens-before procedural order rule:

  1. A happens-before B;
  2. B.
  3. A happens-before C;

The third happens-before relationship here is derived from the transitivity of happens-before. A happens before B, but B is actually executed before A (see the reordered order above). As mentioned in Chapter 1, if A happens-before B, the JMM does not require A to perform before B. The JMM simply requires that the previous operation (the result of the execution) be visible to the latter, and that the former operation precedes the second in order. The result of operation A does not need to be visible to operation B; Moreover, the result of reordering operations A and B is the same as that of operations A and B in happens-before order. In this case, the JMM considers the reordering not illegal, and the JMM allows it. In computers, software technology and hardware technology have a common goal: to develop as much parallelism as possible without changing the results of program execution. Compilers and processors follow this goal, and as you can see from the definition of happens-before, the JMM follows this goal as well.

The impact of reordering on multithreading

/** * reorder */ class ReorderExample {int a = 0; boolean flag =false;

	public void writer() {
		a = 1; // 1
		flag = true; // 2
		System.out.println("writer");
	}

	public void reader() {
		if (flag) { // 3
			int i = a * a; // 4
			System.out.println("i:" + i);
		}
		System.out.println("reader");
	}

	public static void main(String[] args) {
		ReorderExample reorderExample = new ReorderExample();
		Thread t1 = new Thread(new Runnable() {

			@Override
			public void run() { reorderExample.writer(); }}); Thread t2 = new Thread(newRunnable() {

			@Override
			public void run() { reorderExample.reader(); }}); t1.start(); t2.start(); }}Copy the code

The flag variable is a flag that indicates whether variable A has been written. Here we assume that we have two threads A and B, with A first executing writer() and then THREAD B executing reader(). When thread B performs operation 4, can thread A see that thread A is writing to the shared variable A in operation 1? The answer is: not necessarily. Since operations 1 and 2 have no data dependencies, the compiler and processor can reorder these two operations; Similarly, operations 3 and 4 have no data dependencies, and the compiler and processor can also reorder these two operations. Let’s first look at what might happen when operations 1 and 2 reorder. Take a look at the sequence diagram below:

※ Note: In this paper, red virtual arrow line represents wrong read operation, and green virtual arrow line represents correct read operation.

Now let’s see what happens when operations 3 and 4 are reordered (with this reordering, we can incidentally illustrate control dependencies). The following is a sequence diagram of the program after reordering operations 3 and 4:

As we can see from the figure, the guess execution essentially reorders operations 3 and 4. Reordering breaks the semantics of multithreaded programs here!

In a single-threaded program, reordering operations with control-dependencies does not change the execution result (which is why the as-if-serial semantics allow reordering operations with control-dependencies). But in multithreaded programs, reordering operations that have control dependencies may change the program’s execution results.

This article code address