Article series

How to solve the visibility and ordering problem in Java

Abstract

In the previous article 02, we solved the visibility and order problems in Java, so there is another atomic problem that we have not solved. In the first article 01, the Bug source of concurrent programming, it was mentioned that the feature of one or more operations being executed by THE CPU without being interrupted is called atomicity, so how to solve the problem of atomicity.

It’s important that only one thread executes this condition at a time, which we call mutually exclusive, and if you can protect changes to shared variables that are mutually exclusive, then you can preserve atomicity.

Easy to lock

A block of code that requires mutual exclusion is called a critical section. Before a thread enters a critical section, it first attempts to acquire a lock. If the lock is successful, it can enter the critical section and execute the code. The diagram below:

But there are two things we need to understand: what are our locks? What is it to protect?

Improved lock model

In the world of concurrent programming, there is a correspondence between locks and the resources to be protected by locks. First, we need to mark the resource R to be protected by the critical section. Then, we need to create a lock LR for this resource. Finally, we need to add lock(LR) and unlock(LR) operation when entering and leaving the critical section. As follows:

Java language lock technology: Synchronized

Synchronized modifies methods and code blocks. Lock () and unlock() are automatically added before and after synchronized methods or code blocks. The advantage of this is that locks and unlocks occur in pairs, as forgetting to unlock() can cause other threads to die. So how do we lock up resources that need to be protected? In the following code, the non-static method add1() locks this object (the current instance object) and the static method add2() locks x.class (the current Class object)

public class X {
    public synchronized void add1(a) {
        / / critical region
    }
    public synchronized static void add2(a) {
        / / critical region}}Copy the code

The above code can be understood as follows:

public class X {
    public synchronized(this) void add(a) {
        / / critical region
    }
    public synchronized(X.class) static void add2(a) {
        / / critical region}}Copy the code

Solve count += 1 problem with synchronized

In the 01 concurrent programming Bug source article, we mentioned the concurrency problem with count += 1, and now we try to solve this problem with synchronized.

public class Calc {
    private int value = 0;
    public synchronized int get(a) {
        return value;
    }
    public synchronized void addOne(a) {
        value += 1; }}Copy the code

If the addOne() method is modified by synchronized, only one thread can execute it, so atomicity is guaranteed. What about visibility? In our previous article, 02 How Java solves visibility and order problems, we looked at the locking rule in a pipe. A lock is unlocked happens-before it is unlocked later. Synchronized, in this case synchronized(introduced in a subsequent article). According to this rule, a value += 1 operation performed by the previous thread is visible to subsequent threads. The get() method must also be qualified with synchronized, otherwise it cannot be guaranteed visibility. The example above is shown below:

The get() method protects the value of the resource by locking this object, and the addOne() method protects the value of the resource by locking calc.class.

public class Calc {
    private static int value = 0;
    public synchronized int get(a) {
        return value;
    }
    public static synchronized void addOne(a) {
        value += 1; }}Copy the code

The above example is represented by a graph:

In this example, the get() method uses this lock and the addOne() method uses calc.class lock, so the two critical sections are not mutually exclusive and changes to the addOne() method are invisible to the get() method, leading to concurrency problems. Conclusion: It is not possible to use multiple locks to protect one resource, but it is possible to use one lock to protect multiple resources.

Protects multiple resources that are not associated

In the banking business, changing the password and withdrawing money are the two most common operations, changing the password operation and withdrawing money are unrelated, we can use different mutex to solve the concurrency problem without related resources. The code is as follows:

public class Account {
    // Lock to protect password
    private final Object pwLock = new Object();
    / / password
    private String password;

    // Lock to protect balance
    private final Object moneyLock = new Object();
    / / the balance
    private Long money;

    public void updatePassword(String password) {
        synchronized (pwLock) {
            // Change the password}}public void withdrawals(Long money) {
        synchronized (moneyLock) {
            / / withdrawals}}}Copy the code

Use pwLock and moneyLock to protect passwords and balances, respectively, so that password and balance changes can be made in parallel. Fine-grained locking improves performance by using different locks to finer manage protected resources. In this example, you may notice that I used a final Object as a lock. Here’s how: Using locks must be immutable objects. When using a mutable object as a lock, it is equivalent to changing the lock when the mutable object is modified, and when using Long or Integer as a lock, caching is used between -128 and 127. See their valueOf() method for details.

Protects multiple resources with associated relationships

In banking, besides the operation of changing password and withdrawing money, there is another function with more operation is transfer. If account A transfers $100 to account B, account A’s balance decreases by $100 and account B’s balance increases by $100, then the two accounts are related. Before you understand the mutex, you might write something like this:

public class Account {
    / / the balance
    private Long money;
    public synchronized void transfer(Account target, Long money) {
        this.money -= money;
        if (this.money < 0) {
            // throw exception} target.money += money; }}Copy the code

In the transfer method, this object (user A) is locked, so can the target user (user B) be locked? Of course not. The two objects are unrelated. The correct action should be to obtain this lock and target lock to transfer the money, the correct code is as follows:

public class Account {
    / / the balance
    private Long money;
    public synchronized void transfer(Account target, Long money) {
        synchronized(this) {
            synchronized (target) {
                this.money -= money;
                if (this.money < 0) {
                    // throw exception} target.money += money; }}}}Copy the code

In this case, we need to be clear about what resources we want to protect, as long as our lock covers all protected resources. But you think this is a perfect example? That’s wrong. There’s a good chance of a deadlock. Can you see that? I’ll use this example to talk about deadlocks in the next article.

conclusion

The most important thing with mutex is: What is our lock? What resource does the lock protect? It’s easy to get those two things straight. And the lock must be immutable. Fine-grained locks are used to protect different resources to improve management and performance.

Geek Time: Java Concurrent Programming Practice 03 Mutex (part 1)

Personal blog: colablog.cn/

If my article helps you, you can follow my wechat official number and share the article with you as soon as possible