In this article, we focus on using mutex to solve atomicity problems in concurrent programming.

An overview of the

The source of the atomicity problem in concurrent programming is thread switching. Can banning thread switching solve the atomicity problem?

This needs to be discussed separately. In the case of a single-core CPU, there is only one thread executing at a time. Disabling CPU interrupts means that the operating system does not reschedule threads and thus forbade thread switching, so that the threads that acquire CPU usage can execute continuously.

In the case of multi-core CPU, there may be two threads executing at the same time, one thread executing on CPU-1 and the other thread executing on CPU-2. In this case, CPU interruption is prohibited, and only one thread executing on a CERTAIN CPU can be guaranteed, but only one thread can be running.

Only one thread is executing at a time, which we call mutually exclusive. If we can guarantee that changes to shared variables are mutually exclusive, then atomicity is guaranteed for both single-core and multi-core cpus.

How do you do that? The answer is mutex.

The mutex model

Simple model of mutex

When we talk about mutex, we usually refer to a section of code that requires mutex execution as a critical section. Here is a simple diagram.When a thread enters a critical section, it first tries to lock. If it succeeds, it can enter the critical section. If it fails, it needs to wait. The thread releases the lock when the critical section of code completes execution or when an exception occurs.

Mutexes improved model

The above model, while intuitive, is too simple, and we need to consider two questions:

  • What are we locking up?
  • What are we protecting?

In the real world, there is a corresponding relationship between locks and the resources to be protected by locks. Colloquially speaking, you use your locks to protect your things, and I use my locks to protect my things.

In the world of concurrent programming, locks and resources should have a similar correspondence.

Here is the improved lock model.First, we need to label the resource R to be protected in the critical region. Then, we create a lock LR for resource R. Finally, we need to lock and unlock the lock LR when we enter and leave the critical region.

By doing this, we can establish a relationship between the lock and the resource, and there will be no “use my home lock to protect your home resource” problem.

Mutex in the Java world

In the Java language, mutex is implemented with the synchronized keyword.

The synchronized keyword can be applied to methods or directly to code blocks.

Let’s look at the sample code below.

Public class SynchronizedDemo {synchronized void updateData() {// Service code} // Synchronized static void RetrieveData () {// business code} // Modify code block Object obj = new Object(); retrieveData() {// business code} // Modify code block Object obj = new Object(); Void createData() {synchronized(obj) {// synchronized(obj)}}Copy the code

In contrast to the mutex model we describe, we don’t see any locking and unlocking code in the above code because the Java compiler automatically adds locking and unlocking logic before and after methods or blocks of code that we modify with the synchronized keyword. The advantage of this is that we don’t have to worry about forgetting to learn about locking after we perform the locking operation.

The lock and lock object of synchronized

When we use the synchronized keyword, what does it lock on? If the lock object is not explicitly specified, Java has the following default rules

  • When you modify a static method, you lock the Class object of the current Class.
  • When you modify a non-static method, the current instance object this is locked.

According to the above rules, the following code is equivalent.

Synchronized void updateData() {synchronized(this) void updateData2() {// Synchronized (this) void updateData2() {// synchronized(this) void updateData2() {// synchronized(this) void updateData2();Copy the code
Synchronized static void retrieveData() {// business code} // Synchronized (synchronizedDemo.class) static void RetrieveData2 () {// Business code}Copy the code

Synchronized sample

We described the count=count+1 example in an earlier article, where no concurrency control was done and the resulting atomicity problem, and now look at how the synchronized keyword can be used to solve the concurrency problem.

First, to review the happens-before rule, synchronized critical sections are mutually exclusive, meaning that only one thread executes a critical section at a time, In happens-before, “unlocking a lock Happens to a subsequent lock Before”, which means that the unlocking operation of the previous thread is visible to the locking operation of the next thread, and then combined with the transitivity principle of happens-before, The shared variables that the previous thread modified in the critical section are visible to subsequent threads that have completed locking and entered the critical section.

Here is the modified code:

public class ConcurrencySafeAddDemo { private long count = 0; private synchronized void safeAdd() { int index = 0; while (index < 10000) { count = count + 1; index++; } } private void reset() { this.count = 0; } private void addTest() throws InterruptedException { List<Thread> threads = new ArrayList<Thread>(); for (int i = 0; i < 6; i++) { threads.add(new Thread(() -> { this.safeAdd(); })); } for (Thread thread : threads) { thread.start(); } for (Thread thread : threads) { thread.join(); } threads.clear(); System.out.println(String.format("Count is %s", count)); } public static void main(String[] args) throws InterruptedException { ConcurrencySafeAddDemo demoObj = new ConcurrencySafeAddDemo(); for (int i = 0; i < 10; i++) { demoObj.addTest(); demoObj.reset(); }}}Copy the code

The result is as follows:

Count is 60000
Count is 60000
Count is 60000
Count is 60000
Count is 60000
Count is 60000
Count is 60000
Count is 60000
Count is 60000
Count is 60000
Copy the code

This is consistent with our expectations.

Compared to the first version of the code, we simply decorated the safeAdd() method with the synchronized keyword.

The relationship between locks and protected resources

In the case of mutex, the relationship between the lock and the protected resource is very important. What is the relationship between the two? A reasonable explanation is that the relationship between locks and protected resources is N:1.

  • A lock can be applied to multiple protected resources
  • There can be only one lock on a protected resource

We can use the analogy of tickets to a football game, where seats are resources and tickets are locks. One seat can only be protected by one ticket. In the case of “reservation”, one reservation ticket can correspond to multiple seats. There will not be multiple tickets for one seat.

Similarly, in the case of mutex, if two locks use different lock objects, then the corresponding critical sections are not mutually exclusive. This is important, and ignoring it can easily lead to inexplicable concurrency problems.

For example, if we change the safeAdd() method in the example code above to something like this, does it still work?

private void safeAdd() { int index = 0; synchronized(new Object()) { while (index < 10000) { count = count + 1; index++; }}}Copy the code

Here, when setting the lock Object for synchronized keyword, we create a new Object each time, so that each thread will use a different lock Object when running here, so the code in the critical area is not mutually exclusive, and the final result will not be what we expect.

Count is 17355
Count is 18215
Count is 19244
Count is 20863
Count is 60000
Count is 60000
Count is 60000
Count is 20430
Count is 60000
Count is 60000
Copy the code

A lock protects multiple resources

We mentioned above that a mutex can protect multiple resources, but a resource cannot be protected by multiple mutex.

So how do we protect multiple resources with a single lock?

A lock protects multiple resources that are not associated

For multiple resources that are not related to each other, it is easy to use a lock to protect them.

Take a bank account as an example. A bank account can be used for withdrawals and password changes, so the account balance and the account password are two unrelated resources.

Let’s look at the sample code below.

public class BankAccountLockDemo { private double balance; private String password; private Object commonLockObj = new Object(); // Private void deposite money (double amount) {synchronized(commonLockObj) {// Balance = balance-amount; Private void changePassword(String newPassword) {synchronized(commonLockObj) {// Service code password = newPassword; }}}Copy the code

As you can see, the code above uses the shared lock commonLockObj to protect balance and password, and it works.

However, the problem is that the withdrawal and password change operations cannot be performed at the same time. From the point of view of services, the two services are unrelated and should be parallel.

The solution is for each business to use its own mutex to protect related resources. In the above code, you can create two lock objects, balanceLockObj and passwordLockObj, so that the two business operations do not affect each other. Such a lock is also known as a fine-grained lock.

A lock protects multiple associated resources

For resources with associated relationships, the situation is more complicated.

Let’s take the transfer operation as an example. The transfer process involves the balance of two accounts, which are two related resources.

Let’s look at the sample code below.

public class BankAccountTransferLockDemo { private double balance; private Object lockObj = new Object(); private void transfer(BankAccountTransferLockDemo sourceAccount, BankAccountTransferLockDemo targetAccount, double amount) { synchronized(lockObj) { sourceAccount.balance = sourceAccount.balance - amount; targetAccount.balance = targetAccount.balance + amount; }}}Copy the code

Is there a problem with the above code? The answer is yes.

It seems that we use lock processing when operating balance, but it should be noted that the lock Object here is lockObj, which is an Object Object. If other services also need to operate balance of the same account, such as deposit and withdrawal operation, other services cannot use lockObj to create locks. As a result, multiple services operate balance at the same time, causing concurrency problems.

The solution to the problem is that we need to create locks that cover all scenarios of the protected resource.

Returning to our example above, if using an Object as a lock Object does not cover all related services, then we need to upgrade the lock Object from an Object Object to a Class Object, as follows:

private void transfer(BankAccountTransferLockDemo sourceAccount, BankAccountTransferLockDemo targetAccount, double amount) { synchronized(BankAccountTransferLockDemo.class) { sourceAccount.balance = sourceAccount.balance - amount; targetAccount.balance = targetAccount.balance + amount; }}Copy the code

The relationship between the resources described above, in more specific and technical terms, is a characteristic of “atomicity” that has two meanings: 1) ATOMicity at the CPU instruction level and 2) atomicity at the business level.

What is the nature of atomicity?

The appearance of atomicity is indivisible, and its essence is that there is a requirement of consistency between multiple resources, and the intermediate state of operation is invisible to the outside.

The solution to the atomicity problem is to make the intermediate state invisible to the outside world, which is the problem mutex solves.