[TOC]

Concurrent programming

1. Source of concurrent programming BUG

There are three cases of visibility, atomicity and order. Visibility issues due to caching, atomicity issues due to thread switching, orderliness issues due to compilation optimizations.

  • Visibility problems caused by caching

Support for multiprocess time-sharing is a milestone in the history of operating systems, and Unix is best known for solving this problem

  • Atomicity issues with thread switching

We call atomicity the property of one or more operations being executed by the CPU without interruption

  • Order problems with compiler optimization

2. Memory model

The Java Memory model specifies how the JVM provides ways to disable caching and compilation optimizations on demand. Specifically, these methods include the keywords volatile, synchronized, and final, as well as six happens-before rules.

2.1 valatile

We declare a volatile variable, volatile int x = 0, which tells the compiler that reads or writes to this variable must be read or written from memory, not from CPU cache. This semantics may seem fairly straightforward, but it can be confusing when used in practice.

Class VolatileExample {int x = 0; class VolatileExample {VolatileExample = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() {if (v == true) { }}}Copy the code

2.2 happens-before rules

2.2.1 Sequential rules of programs

This rule means that the previous action is happens-before any subsequent action in a thread, in procedural order. Changes made to a variable in front of the program must be visible to subsequent operations.

2.2.2 Volatile variable Rules

A write to a volatile variable is happens-before a subsequent read to that volatile variable.

2.2.3 transitivity

2.2.4 Rules of pipe lock

The unlocking of a lock happens-before the subsequent locking of the lock. A pipe is a generic synchronization primitive. In Java, synchronized is the implementation of a pipe in Java.

2.2.5 Thread start() rule

This one is about thread starting. It means that after main thread A starts child thread B, child thread B can see what the main thread does before it starts child thread B. In other words, if thread A calls thread B’s start() method (that is, starting thread B in thread A), the start() operation is happens-before any operation in thread B. See the following example code for details

Thread B = new Thread(()->{var==77}); Var = 77; B.start();Copy the code

2.2.6 Thread Join () Rule

This one is about thread waiting. It means that the main thread A waits for the child thread B to complete (by calling the join() method of the child thread B), and when the child thread B completes (the join() method of the main thread A returns), the main thread can see the operation of the child thread. By “seeing,” of course, we mean operating on shared variables. In other words, if join() of thread B is called in thread A and returns successfully, then any operation in thread B happens-before the return of the join() operation. See the following example code for details

Thread B = new Thread(()->{var = 66; }); Start (); // start(); All changes to shared variables made by the child thread are visible after the main thread calls b.coin (). In this case, var==66Copy the code

Mutex (part 1) : Solve atomicity problems

The condition “only one thread is executing at a time” is so important that we call it mutually exclusive. If we can ensure that changes to shared variables are mutually exclusive, then atomicity is guaranteed for both single-core and multi-core cpus.

Firstly, we need to mark out the resource to be protected in the critical area. In the figure, an element is added to the critical area: protected resource R; Second, to protect resource R, we need to create a lock for it. Finally, for this lock LR, we also need to add lock operation and unlock operation when entering and leaving the critical region. In addition, I specially use a line to make an association between the lock LR and the protected resource, which is very important. A lot of concurrency bugs occur because we ignore them, and then something like locking our door to protect their property is very difficult to diagnose because subconsciously we think we’ve locked it correctly. figure

Where is the lock() and unlock() in synchronized? In the above code we can see that only the modifier block locks an obj object. This is also an implicit Java rule: when modifying static methods, lock the Class object of the current Class, in this case Class X; When decorating a non-static method, the current instance object this is locked.

Class X {// synchronized void foo() {// synchronized static void bar() {// synchronized static void bar() {// synchronized static void bar()} // synchronized code blocks Object obj = new Object(); Void baz() {synchronized(obj) {// synchronized(obj)}}Copy the code
Class X {// synchronized(x.lass) static void bar() {// critical section}}Copy the code
Synchronized (this) void foo() {// critical section}}Copy the code

Synchronized (addOne()) and get() are synchronized synchronized (value = “value” and addOne() = “value”).

class SafeCalc { static long value = 0L; synchronized long get() { return value; } synchronized static void addOne() { value += 1; }}Copy the code

Mutex (2) : How to use a lock to protect multiple resources

4.1 Protecting Unassociated Resources

The example code is as follows. The Account class Account has two member variables: Account balance and Account password. Withdraw () and view the balance getBalance() operations access the account balance balance, and we create a final object balLock as the lock (analogous to football tickets); While changing the password updatePassword() and viewing the password getPassword() will change the account password password, we create a final object pwLock as the lock (analogous to movie tickets). Different resources are protected by different locks. Of course, we can also use a mutex to protect multiple resources. For example, we can use this lock to manage all resources in the account class: account balance and user password. The specific implementation is very simple, all the methods in the example program are added to the synchronization keyword synchronized, here I will not show. The problem with using a lock, however, is that its performance is so poor that withdrawals, checking balances, changing passwords, and checking passwords are all sequential. With two locks, withdrawals and password changes can be done in parallel. Fine-grained management of protected resources with different locks improves performance. Another name for this type of lock is fine-grained locking.

Class Account {// Lock: private final Object balLock = new Object(); Private Integer balance; Private final Object pwLock = new Object(); // Account password private String password; Void withdraw(Integer amt){synchronized(balLock) {if (this.balance > amt){this.balance -= amt; Integer getBalance() {synchronized(balLock) {return balance; }} // Change password void updatePassword(String pw){synchronized(pwLock) {this.password = pw; String getPassword() {synchronized(pwLock) {return password; }}}Copy the code

4.2 Protecting Associated Resources

Therefore, the above scheme lacks practical feasibility, and we need a better scheme. There is a shared lock using account.class. Account.class is shared by all Account objects, and this object is created when the Java VIRTUAL machine loads the Account class, so we don’t have to worry about its uniqueness. Using account.class as the shared lock eliminates the need to pass in the Account object when creating it, making the code simpler.

class Account { private int balance; // Transfer void transfer(Account target, int amt){ synchronized(Account.class) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; }}}}Copy the code

What is the nature of atomicity? In fact, it is not indivisible. Indivisibility is only an external manifestation. Its essence is that there is a requirement of consistency between multiple resources, and the intermediate state of the operation is invisible to the external. For example, there are intermediate states for writing long variables on A 32-bit machine (only 32 bits out of 64) and intermediate states for A bank transfer operation (account A is reduced by 100 before account B has had time to change). So the solution to the atomicity problem is to make sure that the intermediate state is invisible to the outside world.