How to solve the atomicity problem in JAVA Concurrent Programming

stay Source of concurrent programming BUG In this paper, we first understand the three bug sources of concurrent programming: visibility, atomicity and order. stay How to solve visibility and atomicity In this article, we have roughly understood the solution of visibility and order. Today, it's the last big bug, which is atomicity.

Knowledge review

Lock model


Lock model in JAVA

Lock is a general technical solution. The synchronized keyword provided by Java language is an implementation of lock.

  • synchronized is exclusive lock / exclusive lock, but pay attention! synchronized does not change the feature of CPU time slice switching, but when other threads want to access this resource, they find that the lock has not been released, so they can only wait outside.
  • Synchronized can guarantee atomicity, because after a piece of code is modified by synchronized, only one thread can execute the code, no matter it is a single core CPU or a multi-core CPU, so atomic operation can be guaranteed
  • Synchronized also ensures visibility and order. According to the previous second article: the rules of locks in the management process of the happens before rule: unlocking a lock happens before locks the lock later. That is, the unlocking operation of the previous thread is visible to the locking operation of the next thread. Based on the transitivity principle of happens before, we can conclude that the shared variables modified by the previous thread in the critical area (before unlocking the operation) are visible to the subsequent threads entering the critical area (after locking the operation). -The synchronized keyword can be used to modify static methods, non static methods, or code blocks

The theory is finished, let's do something practical! First, we use synchronized to modify the non-static method to rewrite the code of atomicity in Chapter 1:

    private long count = 0;
    
    // Modifying non static methods when modifying non static methods, the current instance object this is locked.
    // When there are multiple ordinary methods in this class modified by Synchronized, the locks of these methods are all objects of this class. When multiple threads access these methods, if these threads use the same object of this class when calling methods, although they access different methods, they use the same object to call them, then the locks of these methods are the same, that is, this object, which will cause blocking. If multiple threads call methods through different objects, their locks are different and will not block.
    private synchronized void add10K(){
        int start = 0;
        while (start ++ < 10000){
            this.count ++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestSynchronized2 test = new TestSynchronized2();
        // Create two threads and perform add() operation
        Thread th1 = new Thread(()->{
            test.add10K();
        });
        Thread th2 = new Thread(()->{
            test.add10K();
        });
        // Start two threads
        th1.start();th2.start();
        // Wait for two threads to finish executing
        th1.join();th2.join();
        System.out.println(test.count);
    }

Run it! You will find that you can always achieve the effect we want~
In addition to modifying non static methods in the above code, you can also modify static methods and code blocks

    // Modify static method when modifying a static method, the Class object of the current Class, TestSynchronized2.class, is locked. This range is larger than the object lock. Even if it's a different object, it uses the same lock as long as it's an object of this Class.
    synchronized static void bar() {
        // Critical area
    }
    // Modifying the classic double lock checking mechanism in code block java
    private volatile static TestSynchronized2 instance;
    public static TestSynchronized2 getInstance() {
        if (instance == null) {
            synchronized (TestSynchronized2.class) {
                if (instance == null) {
                    instance = new TestSynchronized2();
                }
            }
        }
        return instance;
    }

Clarify the relationship between locks and resources

By analyzing the relationship between the locked object and the protected resource, and considering the access path of the protected resource comprehensively, we can make good use of the mutually exclusive lock. The relationship between protected resources and locks is N:1. If a resource uses N locks, there must be something wrong. It's like a toilet pit. You have 10 keys. Isn't it possible for 10 people to enter at the same time?

Now give two pieces of error code, think about what's wrong?

    static long value1 = 0L;

    synchronized long get1() {
        return value1;
    }

    synchronized static void addOne1() {
        value1 += 1;
    }
    long value = 0L;

    long get() {
        synchronized (new Object()) {
            return value;
        }
    }

Reason for the first error:
Because we said that synchronized decorated ordinary methods lock the current instance object this while static methods lock the Class object of the current Class
So here are two locks: this and testsynchronized 3. Class
Because the critical area get() and addOne() are protected by two locks, there is no mutual exclusion between the two critical areas, and the modification of value by the critical area addOne() does not guarantee the visibility of the critical area get(), which leads to concurrency problems.

Reason for the second error:
The essence of locking is to write the current thread id in the object header of the lock object, but synchronized (new Object()) is a new object every time in memory, so locking is invalid.

Q: in the previous examples, multiple locks are used to protect one resource, which is not enough. Can a lock protect multiple resources?

A: if multiple resources are not related to each other, a lock can be used to protect them. If there's a connection, it's not going to work. For example, in the bank transfer operation, you transfer money to me. My account is more than 100, and your account is less than 100. I can't use my lock to protect you, just like in real life, my lock can't protect your property.

Focus! To distinguish whether multiple resources are associated! But a lock can protect multiple unrelated resources. Its performance is poor. For example, I can listen to songs and play games at the same time. You have to let me finish one and then do the other. Doesn't it take twice as long. So even if a lock can protect multiple unrelated resources, but generally, it will use different locks, which can improve performance. This kind of lock also has a name, which is called fine-grained lock.

Q: just about the case of bank transfer, if one day such A thing happens in A bank at the same time, teller Xiao Wang needs to complete the transfer of 100 yuan from account A to account B, and teller Xiao Li needs to complete the transfer of 100 yuan from account B to account A, how can I achieve it?

Answer: in fact, it is realized by two locks. Turn one out and turn the other in. The transfer operation is performed only if both are successful.

    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(200); //Initial account balance of A 200
        Account b = new Account(300); //B's initial account balance 200
        Thread threadA = new Thread(()->{
            try {
                transfer(a,b,100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread threadB = new Thread(()->{
            try {
                transfer(b,a,100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        threadA.start();
        threadB.start();
    }

    static void transfer(Account source,Account target, int amt) throws InterruptedException {
        synchronized (source) {
            log.info("Hold lock{} Wait for lock{}",source,target);
            synchronized (target) {
                if (source.getBalance() > amt) {
                    source.setBalance(source.getBalance() - amt);
                    target.setBalance(target.getBalance() + amt);
                }
            }
        }
    }

At this point, Congratulations, a wave of problems have been solved, but I'm sorry to tell you that another bug has been caused. This code is likely to deadlock! There are so many things to pay attention to in concurrent programming. Let's remember the word deadlock first! Continue to pay attention to the "fat pig learning programming" official account! Find the answer in our later article!

How to guarantee atomicity

Now that we know that mutexes guarantee atomicity, we know how to use synchronized to guarantee atomicity. But synchronized is not the only way to guarantee atomicity in JAVA.

If you take a cursory look at J.U.C(java.util.concurrent package), you can find them conspicuously:

One is lock package, one is atomic package, as long as you have passed CET-4.. I believe you can all immediately conclude that they can solve the problem of atomicity.

Since these two packages are more important, the modules placed behind will be kept alone, paying attention to the official account of the pig.

Tags: Java Programming less

Posted on Mon, 11 May 2020 15:31:43 -0400 by xSN1PERxEL1TEx