How to ensure thread safety in Java

Posted by seddonym on Thu, 16 Dec 2021 06:36:14 +0100

1, Thread safety is embodied in three aspects

1. Atomicity: provides mutually exclusive access. Only one thread can operate on data at a time (atomic,synchronized);

2. Visibility: a thread's modification of main memory can be seen by other threads in time (synchronized,volatile);

3. Orderliness: one thread observes the execution order of instructions in other threads. Due to the reordering of instructions, the observation results are generally disordered (happens before principle).

Next, it is analyzed in turn.

2, Atomicity -- atomic

JDK provides many atomic classes, AtomicInteger,AtomicLong,AtomicBoolean and so on.

They are atomic through CAS.

Let's take a look at AtomicInteger, AtomicStampedReference, AtomicLongArray, and AtomicBoolean.

(1)AtomicInteger

Let's take a look at an example of AtomicInteger:

public class AtomicIntegerExample1 {
    // Total requests
    public static int clientTotal = 5000;
    // Number of threads executing concurrently
    public static int threadTotal = 200;
 
    public static AtomicInteger count = new AtomicInteger(0);
 
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();//Get thread pool
        final Semaphore semaphore = new Semaphore(threadTotal);//Define semaphore
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count.get());
    }
 
    private static void add() {
        count.incrementAndGet();
    }
}

We can see that the final result is that 5000 is thread safe.

Look at the incrementAndGet() method of AtomicInteger:

Take another look at the getAndAddInt() method:

The compareAndSwapInt() method is called:

It is modified by native and represents the underlying method of java, which is not implemented through java.

Look again at getAndAddInt(), and the first value passed is the current object, such as count Incrementandget(), then in getAndAddInt(), var1 is count, and the second value of var2 is the current value. For example, if you want to perform the operation of 2 + 1 = 3, the second parameter is 2 and the third parameter is 1.

Variable 5 (var5) is the current value of the underlying layer obtained by calling the underlying method. If no other thread comes to process our count variable, its normal return value is 2.

Therefore, the parameters passed to the compareAndSwapInt method are (count object, current value 2, current 2 passed from the bottom layer, the value taken from the bottom layer plus the modified variable var4).

The goal of compareAndSwapInt() is to update the current value var2 to the following value (var5+var4) for var1 objects if it is equal to the underlying value var5

The core of compareAndSwapInt is the core of CAS.

Why the count value is different from the underlying value: the value in count is equivalent to the value existing in working memory, and the underlying is main memory.

(2)AtomicStampedReference

Next, let's take a look at the AtomicStampedReference.

There is an ABA problem about CAS: it was first A, then B, and now A. The solution is: each time the variable changes, add 1 to the version number of the variable.

This uses the AtomicStampedReference.

Let's look at the compareAndSet() implementation in AtomicStampedReference:

In AtomicInteger, compareAndSet() implements:

You can see that there is an additional stamp comparison (i.e. version) in compareAndSet() in AtomicStampedReference. This value is maintained during each update.

(3)AtomicLongArray

In this atomic class that maintains an array, we can selectively update the value corresponding to an index, which is also an atomic operation. Various methods of this operation on the array will have one index at multiple places.

For example, let's take a look at compareAndSet():

(4)AtomicBoolean

Look at a piece of code:

public class AtomicBooleanExample {
 
    private static AtomicBoolean isHappened = new AtomicBoolean(false);
 
    // Total requests
    public static int clientTotal = 5000;
    // Number of threads executing concurrently
    public static int threadTotal = 200;
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    test();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("isHappened:{}", isHappened.get());
    }
    private static void test() {
        if (isHappened.compareAndSet(false, true)) {
            log.info("execute");
        }
    }
}

After execution, it is found that log info("execute"); Executed only once, and isHappend value is true.

The reason is that after compareAndSet() for the first time, isHappend becomes true without other thread interference.

By using AtomicBoolean, we can make a piece of code execute only once.

3, Atomicity -- synchronized

synchronized is a kind of synchronous lock, which realizes atomic operation through lock.

The JDK provides two types of locks: one is synchronized, which relies on the JVM to implement the LOCK. Therefore, within the scope of the keyword object, only one thread can operate at the same time; The other is LOCK, which is a code level LOCK provided by JDK and depends on CPU instructions. The representative is ReentrantLock.

There are four types of synchronized decorated objects:

  • Modify the code block and act on the called object;
  • Modifier method, which acts on the called object;
  • Modify static methods to act on all objects;
  • Modifier class that acts on all objects.

Decorating code blocks and methods:

@Slf4j
public class SynchronizedExample1 {
 
    // Decorate a code block
    public void test1(int j) {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                log.info("test1 {} - {}", j, i);
            }
        }
    }
 
    // Modify a method
    public synchronized void test2(int j) {
        for (int i = 0; i < 10; i++) {
            log.info("test2 {} - {}", j, i);
        }
    }
 
    public static void main(String[] args) {
        SynchronizedExample1 example1 = new SynchronizedExample1();
        SynchronizedExample1 example2 = new SynchronizedExample1();
        ExecutorService executorService = Executors.newCachedThreadPool();
        //one
        executorService.execute(() -> {
            example1.test1(1);
        });
        executorService.execute(() -> {
            example1.test1(2);
        });
        //two
        executorService.execute(() -> {
            example2.test2(1);
        });
        executorService.execute(() -> {
            example2.test2(2);
        });
        //three
        executorService.execute(() -> {
            example1.test1(1);
        });
        executorService.execute(() -> {
            example2.test1(2);
        });
    }
}

After execution, you can see that for case 1, test1 internal method block acts on example1. First execute 0-9 output, and then execute the next 0-9 output; Case 2, similar to case 1, acts on example2; In case 3, you can see the cross execution. Test1 acts independently on example1 and example2 without affecting each other.

Modify static methods and classes:

@Slf4j
public class SynchronizedExample2 {
 
    // Decorate a class
    public static void test1(int j) {
        synchronized (SynchronizedExample2.class) {
            for (int i = 0; i < 10; i++) {
                log.info("test1 {} - {}", j, i);
            }
        }
    }
 
    // Modify a static method
    public static synchronized void test2(int j) {
        for (int i = 0; i < 10; i++) {
            log.info("test2 {} - {}", j, i);
        }
    }
 
    public static void main(String[] args) {
        SynchronizedExample2 example1 = new SynchronizedExample2();
        SynchronizedExample2 example2 = new SynchronizedExample2();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            example1.test1(1);
        });
        executorService.execute(() -> {
            example2.test1(2);
        });
    }
}

test1 and test2 will lock the class of the object calling them, and only one object is executing at the same time.

4, Visibility -- volatile

For visibility, the JVM provides synchronized and volatile. Here we look at volatile.

(1) volatile visibility is achieved through memory barriers and prohibition of reordering

volatile will add a store barrier instruction after the write operation during the write operation to refresh the shared variable value in the local memory to the main memory:

When volatile performs a read operation, a load instruction will be added before the read operation to read the shared variables from memory:

(2) However, volatile is not atomic, and it is not safe to perform + + operations

@Slf4j
public class VolatileExample {
 
    // Total requests
    public static int clientTotal = 5000;
 
    // Number of threads executing concurrently
    public static int threadTotal = 200;
 
    public static volatile int count = 0;
 
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count);
    }
 
    private static void add() {
        count++;
    }
}

It is found that the thread is unsafe after execution. The reason is that there are three steps to execute conut + +. The first step is to take out the current memory count value. At this time, the count value is the latest. Next, two steps are performed, namely + 1 and rewriting back to main memory. Suppose that two threads are executing count + + at the same time, and both memory execute the first step. For example, the current count value is 5, and they both read it. Then the two threads execute + 1 and write it back to main memory respectively. In this way, the operation of adding one at a time is lost.

(3) Scenarios for volatile

Since volatile is not suitable for counting, what scenarios are volatile suitable for:

  1. Writes to variables do not depend on the current value
  2. This variable is not included in the formula with other variables unchanged

Therefore, volatile applies to the state tag amount:

Thread 1 is responsible for initialization. Thread 2 constantly queries the initialized value. When thread 1 completes initialization, thread 2 can detect that the initialized value is true.

5, Order

Orderliness means that in JMM, the compiler and processor are allowed to reorder instructions, but the reordering process will not affect the execution of single threaded programs, but will affect the correctness of multi-threaded concurrent execution.

The order can be guaranteed through volatile, synchronized and lock.

In addition, JMM has inherent order, that is, it can be guaranteed without any means. This is called the happens before principle.

If the execution order of two operations cannot be deduced from the happens before principle, they cannot guarantee their order. Virtual machines can reorder them at will.

Happens before principle:

  1. Program order rule: in a single thread, execute in the order of program code writing.
  2. Locking rule: an unlock operation happen s - before the lock operation on the same lock.
  3. Volatile variable rules: write operations on a volatile variable happen before the subsequent read operations on the variable.
  4. Thread start rule: the start() method of thread object happen s - before every action of this thread.
  5. Thread termination rule: all operations of a thread are happy before the termination of this thread can be detected through thread End of join() method, thread The return value of isalive() and other means detect that the thread has terminated execution.
  6. Thread interrupt rule: the call to thread interrupt() method happen s before the event occurs when the code of the interrupted thread detects an interrupt.
  7. Object termination rule: the initialization of an object is completed (the execution of the constructor ends) happen - before the beginning of its finalize() method.
  8. Transitivity: if you operate A happen before operation B and B happen before operation C, you can get A happen before operation C.

If there are friends who need to get free information, You can click here to get the information!

Topics: Java Back-end Programmer architecture