Thread safety – synchronized and volatile

Article directory

  • thread safety
    • 1. What is a thread safety issue
    • 2. Thread unsafe instance
    • Three, thread unsafe reasons to solve
      • 1. Atomicity
        • 1.1 Definition
        • 1.2 Reasons for insecurity
        • 1.3 synchronized keyword
        • 1.4 synchronized feature
        • 1.5 synchronized use
        • 1.6 Modification example
      • 2. Memory visibility
        • 1.1 Example
        • 1.2 Reasons for insecurity
        • 1.3 volatile keyword
        • 1.4 Modified example
      • 3 Instruction reordering
        • 1.1 Function
        • 1.2 Example
    • Summarize

Thread Safety

1. What is thread safety issue

First of all, we need to understand that the scheduling of threads in the operating system is preemptive, or random, which causes the execution order of threads during thread scheduling execution to be uncertain, and some code execution orders do not affect the results of program running. , but there are also some codes whose execution order changes, and the rewritten running results will be affected, which will cause bugs in the program. The code that will cause bugs in the program when multiple threads are concurrent is called thread-unsafe code, which is thread Security Question.

Second, thread unsafe example

public class ThreadDemo9 {<!-- -->
    public static void main(String[] args) throws InterruptedException {<!-- -->
        Counter counter = new Counter();
       
        Thread t1 = new Thread(() -> {<!-- -->
            for (int i = 0; i < 50000; i ++ ) {<!-- -->
                counter. add();
            }
        });
        Thread t2 = new Thread(() -> {<!-- -->
            for (int i = 0; i < 50000; i ++ ) {<!-- -->
                counter. add();
            }
        });
        t1. start();
        t2.start();

        t1. join();
        t2. join();

        System.out.println(counter.get());
    }
}


 class Counter {<!-- -->
    private int count = 0;

     public void add() {<!-- -->
        count + + ;
    }

    public int get() {<!-- -->
        return count;
    }
}

Expected result: 100000

actual results:
image-20230322215109741

Why are the two threads incremented 5w times respectively, but the result is not 10w?

Three, thread unsafe reasons to solve

1. Atomicity

1.1 Definition

A group of operations (one or more lines of code) is the ****indivisible minimum execution unit*, which means that this group of operations is *atomic** **of

*Multiple threads concurrently *operate on a * shared variable* , the operation will be /strong>*not atomic*

1.2 Reasons for insecurity

counter.add() We can split this sentence into 3 operations:

  • load reads a value from memory into a register
  • add for self-increment operation
  • save writes from registers back to memory

Due to the randomness of thread execution, there may be interleaved execution of these three steps (one is adding, the other is loading)

If we fix the three-step operation together through code, we can solve the problem

1.3 synchronized keyword

When you use an ATM in your room, you lock the door so no one else can come in

After you are done using the room, unlock it, and other people can enter the room at this time

img

The most commonly used locking operation in java is to use the synchronized keyword to lock

1.4 synchronized feature

Mutual exclusion

  • Enter the code block modified by synchronized, which is equivalent to locking
  • Exit the synchronized modified code block, which is equivalent to unlock
  • The thread that has not grabbed the lock blocks and waits to participate in the next ‘lock competition’
  • Lock contention
    • Two threads compete for the same lock, resulting in blocking wait
    • Two threads compete for different locks, and there will be no blocking wait

Refresh memory

The working process of synchronized:

  • acquire a mutex
  • Copy the latest variable from main memory to working memory
  • perform operations on variables
  • Flush the value of the modified shared variable to main memory
  • release the mutex

Reentrant

synchronized is a reentrant lock
The same thread can successfully apply for an object lock multiple times

After a thread requests successfully, the JVM will record the thread holding the lock, and set the counter to 1; at this time, other threads request the lock, they must wait; and if the thread holding the lock requests the lock again lock, you can get the lock again, and the counter will increase at the same time; when the thread exits the synchronization code block, the counter will decrement, and if the counter is 0, the lock will be released.

1.5 synchronized use

  • Decorate common methods

public class SynchronizedDemo {

 synchronized public void method() {

? }

}

  • Modified code block

public class SynchronizedDemo {

? public void method() {

 synchronized (**this**) {
  
  }

? }

}

Here this can be replaced with any Object class object, the effect is the same as that of ordinary decoration

  • Decorate static methods

public class SynchronizedDemo {

 synchronized public static void method() {

? }

}

  • Modified code block

public class SynchronizedDemo {

 public void method() {
  
  synchronized (**SynchronizedDemo. class**) {
  
  }

? }

}

  • lock object

Whoever calls the method or class modified by synchronized is the lock object

image-20230322223447723

The counter here is calling the add method, and the counter is the lock object

1.6 Modification example

public void add() {<!-- -->
synchronized(this){<!-- -->
   count + + ;
}
}

Ensure that the three-step operations in counter.add() are executed at the same time, so that the final result can be correct.

The difference between join and lock:

  • join: let a certain thread complete execution, complete serial, low efficiency
  • Locking: some parts are serial, others are parallel
  • Under the premise of ensuring thread safety, the efficiency is higher.

2. Memory visibility

1.1 Example

 public static int flag = 0;
    public static void main(String[] args) {<!-- -->
        Thread t1 = new Thread(() -> {<!-- -->
            while (flag == 0) {<!-- -->
                // empty
            }
            System.out.println("Loop end! t1 end!");
        });

        Thread t2 = new Thread(() -> {<!-- -->
            Scanner scanner = new Scanner(System.in);
            System.out.println("Please enter an integer: ");
            flag = scanner. nextInt();
        });

        t1. start();
        t2.start();
    }

As can be seen from the execution results, the program does not end after we enter a number.

1.2 Reasons for insecurity

The main two steps performed by the program

  • load reads data from memory into a register
  • Whether the value of the cmp compare register is 0

Since the body of the while loop is empty, the execution speed is very fast, far exceeding the speed of comparison, so that the value read out each time is 0, so the compiler actively optimizes, thinking that load reads It will only be 0 when it comes out, which will result in reading data only once, and the comparison will be carried out afterwards. This is why even if we enter a non-zero number, the program cannot be stopped

Compiler optimization

Under the premise of ensuring that the result of the program remains unchanged (multi-threading is not necessarily the case), the efficiency of the program is improved through operations such as addition and subtraction statements.

To solve this problem, we only need to let the compiler not actively optimize

1.3 volatile keyword

Let the compiler not optimize the variable it decorates, and re-read it from memory every time

  • volatile does not guarantee atomicity
  • One thread reads and one thread writes.

image-20230322235016294

Working memory: registers + cache

expand:

  • CPU cache: read speed is between read register and memory

  • cpu read data sequence: register = “cache 1 = “cache 2 = “cache 3 = “memory

1.4 Modification example

public class ThreadDemo14 {<!-- -->
    volatile public static int flag = 0;

    public static void main(String[] args) {<!-- -->
        Thread t1 = new Thread(() -> {<!-- -->
            while (flag == 0) {<!-- -->
                // empty
            }
            System.out.println("Loop end! t1 end!");
        });

        Thread t2 = new Thread(() -> {<!-- -->
            Scanner scanner = new Scanner(System.in);
            System.out.println("Please enter an integer: ");
            flag = scanner. nextInt();
        });

        t1. start();
        t2.start();
    }
}

3 command reordering

1.1 Function

Instruction reordering: On the premise of keeping the overall logic unchanged, adjust the code execution order to improve efficiency.

1.2 Example

image-20230321202510600

adjusted:

image-20230321202606447

Summary

Reasons for thread insecurity:

Threads are preemptively executed, and the scheduling between threads is full of randomness
Multiple threads modify the same variable
Operations on variables are not atomic
Thread safety due to memory visibility
Instruction reordering also affects thread safety

syntaxbug.com © 2021 All Rights Reserved.