java concurrent programming 2

Race Conditions and Synchronization

   1. Race Condition: Two or more threads read and write to a common variable at the same time. Since the execution order cannot be controlled, the result is wrong.

 

  • Computational output: such as the result of a method call
  • Uncontrolled: For example, thread scheduling is uncontrolled and random
  •   Sequence or timing: For example, which thread is executed first, which line of code is executed is switched out by the OS, and that thread is switched in and executed by the CPU

 

The following is a simple example, assuming that there is such a calculator:

 

public class Calculator
{
    private final int[] input;
    private int         start;
    private int         end;
    public Calculator( int start, int end )
    {
        super();
        this.input = new int[] { start, end };
        reset();
    }
    private void reset()
    {
        start = input[0];
        end = input[1];
    }
    public int add()
    {
        int res = 0;
        for ( ; start <= end; start++ )
            res += start;
        reset();
        return res;
    }
}

 We expect it to output the correct result every time it calculates, here are two threads calling the calculator 100 times in a row:

 

 

private static class CalculateThread extends Thread
{
    private Calculator calculator;
 
    public CalculateThread( Calculator calculator )
    {
        super();
        this.calculator = calculator;
    }
 
    public void run()
    {
        for ( int i = 0; i < 1000; i++ )
            System.out.println( calculator.add() );
    }
}
 
public static void main( String[] args )
{
    Calculator calculator = new Calculator( 0, 100 );
    new CalculateThread( calculator ).start(); //Thread 1
    new CalculateThread( calculator ).start(); //Thread 2
}

 Looking at the calculation results, you can see that most of the calls can get the correct sum 5050, but there will be some random values ​​mixed in, such as 0, 4189, 1766... ​​and the output results of each run are different. This is a typical race condition scenario. When thread 1 calls the add() method, it may be suspended at any time due to the thread scheduling of the OS. At this time, the calculator is in an inconsistent intermediate state , and thread 2 starts/continues at this time. In its add() call, the values ​​of the two state fields of the calculator are indeterminate and random, so the calculation result must be unreliable.

 

The reason why most of the results are correct is because the time slice allocated by the CPU to the thread is very long compared to the loop in add(), so in most cases the thread can execute this method at one time without interference .

Even some very simple code can introduce race conditions:

 

public class IntCounterTest {
    private static int counter = 0;
    private static class CounterThread extends Thread {
        public void run() {
            for (int i = 0; i < 10000000; i++)
                synchronized (this) {
                    counter++;
                }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        CounterThread t1 = new CounterThread();
        t1.start();
        CounterThread t2 = new CounterThread();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter);
    }
}

 In the above example, we let the two threads t1 and t2 increase the value of the counter by 10 million respectively. The expected result should be 20 million, but the result is incorrect almost every time it runs. What is the reason? Because the ++ operator consists of at least three instructions:

 

 

  •     Read variables from main memory to local memory
  •     perform variable calculations
  •     Update the result to main memory

  The root cause of a race condition is that an operation (eg, a method call, a computation) is not an atomic operation.

    2、synchronize

   It is to make a java method programming atomic, then it is necessary for the thread to execute this method without interference, that is, other threads will not modify the state of the shared memory. To achieve this goal is to use the Mutex Lock mechanism, which can keep only one thread at a time with access to critical code that modifies the state of a shared resource. The Java language supports the mutual exclusion mechanism--synchronize keyword, we can rewrite the code of the above example:

private static class CounterThread extends Thread
{
    public void run()
    {
        for ( int i = 0; i < 10000000; i++ )
            //Limit only one person can access the object IntCounterTest.class at the same time
            //Because this class object is globally unique, only one thread can increment the counter at the same time
            synchronized ( IntCounterTest.class )
            {
                counter++;
            } //At the boundary of a synchronized block, the values ​​of all register variables are considered invalid
    }
}

 Do this again, and the result is always 20 million. However, the cost of using synchronized is high. In the test on my machine, the difference in the running time of the code before and after modification is more than 100 times. The AtomicInteger provided by Java5 has relatively good performance (still a 10x performance gap) and can be used in place of the int in the above example.

The thread waiting (blocking) at the entry of the synchronized block, the thread of sleep, the thread of wait, from the perspective of the Linux system, are all in the S (interruptible sleep) state.

    volatile

    In Java, the storage and reading of variables other than long and double are atomic operations, so there is no intermediate state between the reading and storage of these variables. Even so, unprotected shared variables are still unsafe, because Java's memory model allows threads to hold copies of local memory (often registers) of shared variables, which means that a thread modifies a Some shared variables may not be discovered directly by other threads. There are two ways to solve this problem:

  1.     Forbid direct access to variables, use the synchronize keyword to protect getter/setter methods
  2.     Variables use volatile life variables

   The second method is more elegant. volatile can be understood as synchronizing the read and write of a single variable (JSR-133). The volatile keyword includes:

   Visibility: Ensure that every read of a variable goes to main memory

   Atomicity: Ensure that every write to a variable directly changes memory

   Ordered: Happens-Before principle, write operations to volatile must occur after subsequent read operations.

 

    The volatile keyword can be used on various variables, even arrays and objects, but for the latter two, the scope defined by volatile is on references (pointers).

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326060761&siteId=291194637