Competitive conditions

In The Essence and Design of Operating Systems, a race condition is defined as: multiple processes or threads read and write data items at the same time, so that the final result depends on the order in which the instructions in those processes are executed.

In other words, in the race condition, the result goes from the determined state in the single thread to the uncertain state in the multi-thread. What is the cause of this uncertainty? Let’s talk about it.

In the case of a single thread, all the code is executed in a predetermined order. For example, if we manipulate a memory area or modify a variable, the result is consistent with the logic of the code.

But when multiple threads are executing at the same time, the situation becomes much more troublesome. Simultaneous execution, or concurrency, is achieved through processor time slicing, where each thread executes a time slice in turn. In this case, a thread may not have completed all of its operations when the time slice runs out, and the processor is switched to another thread halfway through.

Worst of all, if the interrupted thread is manipulating resource A, and the switched thread is also manipulating resource A, then the next time the processor executes the interrupted thread, resource A may have been modified beyond recognition, resulting in problematic results.

We use an example to understand the competition condition.

/**
 * @author: Wray Zheng
 * @date: 2018-02-01
 * @description: An example of multi-thread
 */
public class MyThread implements Runnable {

    private static String globalBuffer = "";
    private String m_msg;

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyThread("A"), "Thread-A");
        Thread t2 = new Thread(new MyThread("B"), "Thread-B");

        t1.start();
        t2.start();
    }

    public MyThread(String msg) {
        m_msg = msg;
    }

    public static void print(String msg) {
        globalBuffer = msg;
        System.out.println(Thread.currentThread().getName() + ": " + globalBuffer);
    }

    @Override
    public void run() {
        try {
            while (true) {
                print(m_msg);
                Thread.sleep(500);
            }
        } catch(Exception e) {}
    }

}
Copy the code

The key points of the sample program are the global variable globalBuffer and the print function print. The print function is very simple. It simply assigns the parameter to globalBuffer and then prints it out.

By the logic of the code, the output should be that thread A prints character A and thread B prints character B. But the result:

Thread-B: A
Thread-A: A
Thread-B: B
Thread-A: A
Thread-A: B
Thread-B: B
Thread-A: A
Thread-B: A

Copy the code

The reason for the output error is that both threads A and B need to read and write to the shared resource globalBuffer, and the process of reading and writing, namely the print function, is not an atomic operation.

Due to the nature of concurrency, thread A may be interrupted after executing the first assignment of print and the processor will execute thread B’s print instead. When we go back to thread A and execute the print function, the globalBuffer has been modified by thread B, so the output is incorrect.

The mutex

To solve the problem caused by competitive conditions, we can lock resources. Resources that are read and written by multiple threads are called shared resources, also called critical resources. Areas of code involved in manipulating Critical resources are called Critical sections. Only one thread can enter a critical section at a time. We call this mutual exclusion, where multiple threads are not allowed to operate on a shared resource at the same time.

How are critical sections mutually exclusive? Let’s go ahead and analyze.

Before entering a critical section, you need to obtain a mutex. If a thread is already using the resource, you need to wait until another thread returns the mutex.

After the shared resource is operated on, when the critical section is exited, the mutex needs to be returned so that other threads waiting to use the resource can enter the critical section.

Examples of pseudocode:

wait(lock); // Get mutex {critical section, operate on shared resource} signal(lock); // Return the mutexCopy the code

A critical section can be locked using ReentrantLock in Java to prevent multiple threads from entering a critical section at the same time:

private static Lock bufferLock = new ReentrantLock(); public static void print(String msg) { bufferLock.lock(); // Critical section, operation critical resource globalBuffer bufferLock.unlock(); }Copy the code

Java.util.concurrent allows you to determine the state of the lock before a critical section and decide whether to block or enter a critical section.

The synchronized keyword

Java provides an easier way to implement critical sections of mutual exclusion.

For example, we can add the synchronized keyword to functions that operate on shared resources:

Public synchronized void myFunction() {synchronized void myFunction();Copy the code

In this way, you can ensure that at most one thread is executing the function at a time. If resource A is only read and written in this function, you can guarantee that resource A will not be read and written by multiple threads simultaneously.

However, if shared resource A is also operated on in other functions, then mutual exclusion of resources cannot be implemented in this way. Because even if these functions are declared synchronized, this only means that multiple threads cannot execute the same function at the same time, but multiple threads are allowed to execute different functions at the same time, and these functions are all operating on the same resource A.

Here we present another way to implement mutual exclusion of resource usage.

Synchronized code block

Only mutual exclusion of function bodies can be achieved by declaring functions as synchronized. To ensure that A resource is mutually exclusive, meaning that only one thread can use the resource at A time, place statements that operate on resource A in A synchronized block:

public void function1() { ...... Synchronized (A) {// Operate resources A}...... } public void function2() { ...... Synchronized (A) {// Operate resources A}...... }Copy the code

Thus, for resource A, only one corresponding block of synchronized code can be executed at A time. Therefore, no matter where resource A is used, there are no multiple threads competing for that resource.

synchronous

Multiple threads work cooperatively on the same resource, which is called synchronization. Synchronization is essentially a collaboration between threads that operate on the same resource.

A famous example is the producer-consumer problem.

Now you have a producer and a consumer, and the producer is responsible for producing the resource and putting it in a box of infinite capacity; The consumer takes resources from the box, and waits if there are no resources in the box.

There are two aspects to this problem: mutual exclusion and synchronization (cooperation). Mutual exclusion means that only one party can use the box at a time. Synchronization means that when consumers consume, they need to meet a condition: the resources in the box are not zero, and this condition requires the cooperation of both parties. In other words, consumers cannot consume resources without limit, but can only consume when the resources produced by producers have surplus.

In this example, the consumer is not only limited by the box mutex, but must wait until the resource is not zero before consuming it.

Pseudocode is used to represent the operations of both sides as follows:

private static Box box = new Box(); private static int boxSize = 0; public static void producer() { wait(box); // put the resource into box, boxSize++ signal(box); } public static void consumer() { while (boxSize == 0); // Block wait(box) if the resource is zero; // Fetch the resource from box, boxSize-- signal(box); } public static void main(String[] args) { parbegin(producer, consumer); // Two functions are executed concurrently by two threads}Copy the code

The condition “resource not zero” needs to be checked before the mutex is acquired; otherwise, if the mutex of the box is acquired before the condition is checked, a deadlock may occur, that is, both threads are permanently blocked.

This is because once the consumer acquires the mutex, if the box does not satisfy the condition “resource not zero”, it blocks, waiting for the producer to produce the resource. Because the box is being used by the consumer, the producer also blocks, waiting for the consumer to run out of the box. Both threads are blocked and can no longer be unblocked.

Condition object

As we mentioned above, resource usage criteria should be used to prevent deadlocks before obtaining the mutex. This is true for normal code logic, but the implementation of locks in Java is more special. You should obtain the mutex first and then determine whether the condition is satisfied. Discard the acquired mutex using the await() method of the Condition object and block until the Condition is satisfied.

In other words, the usual logic is to determine the resource usage condition and obtain the mutex if it is met. The Java logic is to acquire the mutex and discard the acquired mutex if the resource usage condition is not met.

Condition objects are created in Java by calling the Lock object’s newCondition() method, which also means that Condition is associated with Lock. After obtaining the mutex, if the Condition is not met, we use the await() method of the Condition object to discard the acquired lock so that other threads can use the shared resource. At the same time, the current thread starts to wait, and as soon as another thread calls the Condition object’s signalAll() method, the current thread is notified, checks the Condition again, and waits to acquire the mutex if it meets it.

Specific examples are given below:

import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author: Wray Zheng * @date: 2018-02-02 * @description: An example of synchronization between multiple threads */ public class Synchronization { private static int resourceCount = 3; private static Lock boxLock = new ReentrantLock(); private static Condition resourceAvailable = boxLock.newCondition(); public static void main(String[] args) { Thread producer = new Thread(() -> { try { while (true) producer(); } catch (InterruptedException e) {} }); Thread consumer = new Thread(() -> { try { while (true) consumer(); } catch (InterruptedException e) {} }); producer.start(); consumer.start(); } public static void producer() throws InterruptedException { boxLock.lock(); resourceCount++; resourceAvailable.signalAll(); System.out.println("Producer: boxSize + 1 = " + resourceCount); boxLock.unlock(); Thread.sleep(1000); } public static void consumer() throws InterruptedException { boxLock.lock(); try { while (resourceCount == 0) resourceAvailable.await(); resourceCount--; System.out.println("Consumer: boxSize - 1 = " + resourceCount); } finally { boxLock.unlock(); } Thread.sleep(500); }}Copy the code

Two threads are created in the example: a consumer thread and a producer thread. Producers continue to produce resources and consumers continue to consume resources. To verify that the consumer blocks until the producer produces resources again when the resource is zero, let the consumption rate of the consumer be greater than the production rate of the producer and set the initial resource number resourceCount to 3.

When the program is run, the following results are obtained:

Producer: resourceCount + 1 = 4
Consumer: resourceCount - 1 = 3
Consumer: resourceCount - 1 = 2
Producer: resourceCount + 1 = 3
Consumer: resourceCount - 1 = 2
Consumer: resourceCount - 1 = 1
Consumer: resourceCount - 1 = 0
Producer: resourceCount + 1 = 1
Consumer: resourceCount - 1 = 0
Producer: resourceCount + 1 = 1
Consumer: resourceCount - 1 = 0
Producer: resourceCount + 1 = 1
Consumer: resourceCount - 1 = 0
Copy the code

As you can see, resources are consumed much faster than production at first, but after that, consumers have to wait for producers to produce resources each time, so they are forced to block for a while each time, and eventually the consumption rate matches the production rate.

Wait () and notifyAll ()

In fact, Object objects in Java come with methods corresponding to await() and signalAll() of Condition objects: wait() and notifyAll().

Since all objects inherit from objects, we can use locks inside shared resource objects using the synchronized keyword, and use wait() and notifyAll() of objects to implement synchronization between threads.

The previous producer-consumer procedure can be rewritten as follows:

/** * @author: Wray Zheng * @date: 2018-02-02 * @description: An example of synchronization between multiple threads */ public class Synchronization { private static int resourceCount = 3; private static Object box = new Object(); public static void main(String[] args) { Thread producer = new Thread(() -> { try { while (true) producer(); } catch (InterruptedException e) {} }); Thread consumer = new Thread(() -> { try { while (true) consumer(); } catch (InterruptedException e) {} }); producer.start(); consumer.start(); } public static void producer() throws InterruptedException { synchronized (box) { resourceCount++; box.notifyAll(); System.out.println("Producer: resourceCount + 1 = " + resourceCount); } Thread.sleep(1000); } public static void consumer() throws InterruptedException { synchronized (box) { while (resourceCount == 0) box.wait(); resourceCount--; System.out.println("Consumer: resourceCount - 1 = " + resourceCount); } Thread.sleep(500); }}Copy the code

Isn’t it more convenient to use synchronized blocks and their wait() and notifyAll() than to manually create Lock and Condition objects to synchronize threads?

supplement

For functions that declare synchronized, synchronization can be achieved in the following ways:

public synchronized void function() { while (! condition) wait(); // notifyAll(); }Copy the code

conclusion

The mutual exclusion and synchronization of multiple threads in Java is similar to the mutual exclusion and synchronization of processes in operating systems.

In Java, when multiple threads operate on the same resource, lock() and unlock() of the ReentrantLock object can be used to achieve mutual exclusion of the resource, or synchronized code blocks can be used to achieve mutual exclusion. Add the synchronized keyword when declaring a function to achieve mutual exclusion of the function body.

Multithreaded synchronization not only needs to realize the use of resources mutually exclusive, but also needs to meet certain conditions to use.

A Condition object can be obtained from the Lock object’s newCondition() method, and the mutex can be discarded by calling the await() method of the Condition object if the Condition is not used for the resource. Block until the condition is met before acquiring the mutex again.

You can also use Java objects’ wait() and notify() methods, which can be used together with the synchronized keyword to simplify multithreaded synchronization.

Related articles

  • Common usage scenarios for Java Lambda expressions
  • Java GUI: Awt/Swing zoom and scroll images to view
  • The difference between Java and C++ in creating objects through new
  • Eclipse imports Java Web projects created by Maven
  • Java Web: Three diagrams to understand servlets, filters, listeners
  • Best practices for managing multi-module projects with Maven

like