[JavaEE] volatile and wait and notify


Column introduction: JavaEE from entry to advanced

Topic source: leetcode, Niuke, Jianzhi offer.

Creation goal: record the learning process of learning JavaEE

I hope to help others while improving myself, and make progress together with everyone and grow up with each other.

Academic qualifications represent the past, ability represents the present, and learning ability represents the future! 


Table of contents

1. The volatile keyword. 

1. volatile can guarantee memory visibility issues

2. volatile does not guarantee atomicity 

Two.wait and notify

1. wait method

2. notify method

3. The comparison between wait and sleep


1. The volatile keyword. 

1. volatile can guarantee memory visibility issues

  • What is memory visibility?

Visibility means that the modification of memory by a thread can be seen by other threads in time.

Java memory model (JMM): The Java memory model is defined in the Java virtual machine specification, the purpose is to shield the memory access differences of all hardware and operating systems, so that Java programs can achieve consistent concurrency effects on various platforms.

  • Shared variables between threads exist in main memory (Main Memory)
  • Each thread has its own "working memory" (registers)
  • When a thread wants to read a shared variable, it will copy the shared variable from the main memory to the working memory, and then read the data from the working memory.
  • When a thread wants to modify a shared variable, it first modifies the copy in the working memory, and finally synchronizes it to the main memory.

Since each thread has its own working memory, the content of these working memories is equivalent to a copy of the same shared variable. At this time, if the value in the working memory of thread t1 is modified, the working memory of thread t2 may not change in time. This When the code is prone to problems.

Two questions arise at this point:

  • why so much memory
  • Why copy multiple times

1) Why so much memory?

In fact, there is not so much memory, this is just a term in the Java specification, which is the name of the term abstraction.

The so-called main memory is the real memory from the perspective of hardware , and the so-called working memory refers to the registers and caches of the CPU . As for why it is named working memory, on the one hand, it is for simplicity of expression, and on the other hand, it is to avoid involving hardware For example, some CPUs may not have a cache, and some may have many, so Java uses working memory in a nutshell.

2) Why copy multiple times?

Because the speed of CPU access to registers and caches is 3-4 orders of magnitude faster than accessing registers.

If you want to read the same data 10 times in a row, it is very slow to continuously access from the memory, then if you read the register from the memory for the first time, the next 9 reads from the register will be much faster.


  • Variables modified by volatile can guarantee memory visibility.

Code example:

Create two threads t1 and t2. The t1 thread reads the flag repeatedly and quickly, and the t2 thread modifies the flag. According to the expected structure, if we modify the flag in the t2 thread to be non-zero, the t1 thread will end the cycle.

class MyCounter{
    public int flag = 0;
}
public class ThreadDemo2 {
    public static void main(String[] args) {
        MyCounter myCounter = new MyCounter();
        Thread t1 = new Thread(()->{
            while (myCounter.flag == 0){
                //循环重复快速读取
            }
            System.out.println("循环结束");
        });
        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个数");
            myCounter.flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

The result is not in line with our expectations. After modifying the flag, the t1 thread does not end the cycle. 

Through jconsole, it can be seen that the t1 thread is still executing, and the t2 thread has been executed. 

Combined with the memory visibility problem, the answer is obvious. One thread reads and one thread modifies, which will cause thread insecurity. From the perspective of assembly, the execution of the following code is divided into two steps:

  • load reads a value from memory into a register.
  • cmp compares the value of the register with 0, and decides where to execute the next step according to the comparison result (conditional loop instruction)

The above loop operation is in the register, and the execution speed is extremely fast (more than one million times per second). After so many loops, before t2 is actually modified, the execution results obtained by load are the same. On the other hand, compared to the cmp operation, load The speed is very slow, and the result of repeated loading is the same, the JVM will think that no one has changed the value of the flag, and will no longer load the value of the flag from the memory, and directly read the flag saved in the register. At this time, the JVM/ An optimization method of the compiler, but due to the complexity of multi-threading, there may be errors in the judgment.

Solution:

At this time, in order to avoid the above situation, the programmer needs to manually intervene. You can add the volatile keyword to the flag variable. It means to tell the compiler that this variable is "volatile", and you must reload this variable from memory every time. , no more aggressive optimizations can be performed.

class MyCounter{
    public volatile int flag = 0;
}

2. volatile does not guarantee atomicity 

There is an essential difference between volatile and synchronized, synchronized guarantees atomicity, and volatile guarantees memory visibility.

Code example:

This is the code that initially demonstrates thread safety, and the two threads each increment the count 50,000 times.

  • Remove the synchronized keyword that modifies the add method.
  • Add the volatile keyword to the count variable. 

The final code execution result is not the expected 10w times. 

class Counter{
    public volatile int count;
    public void add(){
            count++;
    }
}
public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("count = "+counter.count);
    }

Two.wait and notify

Since the thread is characterized by preemptive execution and random scheduling, the order of execution between threads is difficult to predict, but in actual development, we hope to reasonably coordinate the order of execution among multiple threads.

Accomplishing this coordination mainly involves three methods:

  • wait()/wait(long timeout). Let the current thread enter the waiting state.
  • notify()/notifyAll(). Wake up the method currently waiting on the object.

Tips: wait(), notify(), notifyAll() are all methods of the Object class.

Through the above introduction, it can be found that wait and notify have great overlap with join and sleep in function, so why develop wait and notify?

Because, to use join, you have to wait for one thread to be completely executed before switching to another thread. If we want thread 1 to execute 50%, and then immediately execute thread 2, obviously join cannot achieve this effect. And use sleep must specify how long to sleep Time, but it is not easy to estimate how much time it takes for thread 1 to finish executing. So using wait and notify can better solve the above problems.


1. wait method

  • What wait does:
  • release the lock first
  • block wait
  • After receiving the notification, try to acquire the lock again, and continue to execute after acquiring the lock.

Code example:

 public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        object.wait();
    }

An exception occurs when running this code. This is because to execute the wait operation, the lock of the current thread needs to be acquired first, and the current thread is not locked, so an illegal lock state exception will occur. This is like, a friend of mine has not received the offer. The selection of companies has already begun.

Modified code:

public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized (object) {
            System.out.println("wait 之前");
            object.wait();
            System.out.println("wait 之后");
        }
    }

From the running results, we can know that the code is blocked until object.wait() is executed. In fact, before the blocking state, wait has released the lock. At this time, other threads can acquire the lock of the object object, and wait until wait is awakened. Try to acquire this lock.

For example, the funny old iron goes to the ATM to withdraw money. When he enters the bank branch, he locks the door and starts to operate the ATM. It turns out that the ATM has no money. Since there are people waiting in line outside the bank to handle other businesses, he can only open it. Go out after locking ( equivalent to the operation of waiting to release the lock ), waiting for the cash truck to deposit money ( equivalent to blocking waiting in wait ), when the cash truck deposits money into the bank, the funny old iron standing outside waiting in line, and To compete with others for the opportunity to enter the bank. ( Retry to acquire this lock ), perform a withdrawal operation after entering the bank ( continue to perform other operations after re-locking ).

  • wait The condition for ending the wait
  • Other threads call the object's notify method.
  • wait The waiting time is timed out. (wait has a method with parameters, you can specify the waiting time)
  • Other threads call the Interrupted method of the waiting thread, causing wait to throw an InterruptException .

2. notify method

The notify method is to wake up the waiting thread.

  • The notifty method also needs to be called in the locked method and locked code block . This method is used to wake up the threads that are blocked by calling the wait method, and notify them to reacquire the object lock.
  • If multiple threads call the same object and are waiting, the thread scheduler will randomly select a thread in the wait state to wake up.
  • After the notify method is executed, the current thread will not release the object lock immediately, and the lock object will not be released until the thread executing the notify method completely exits the locking code block.

Code example:

public class ThreadDemo3 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread t1 = new Thread(() -> {
            //这个线程负责进行等待
            System.out.println("t1: wait 之前");
            try {
                synchronized (object) {
                    object.wait();
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("t1: wait 之后");
        });
        Thread t2 = new Thread(() -> {
            System.out.println("t2: notify 之前");
            //notify务必获取锁才能通知
            synchronized (object) {
                object.notify();
            }
            System.out.println("t2: notify 之后");
        });
        t1.start();
//此时让 wait 先执行,防止 notify 空打一炮.
        Thread.sleep(100);
        t2.start();
    }
}

Observe that the code execution results are clearly in line with expectations. 

Why is the notify method also in a synchronous method or a synchronous code block?

A synchronized method or a synchronized code block refers to a locked method or a locked code block.

Code example:

Suppose we want to implement a blocking queue, if no synchronization code block is added, the implementation method is as follows:

class BlockingQueen{
    Queue<String> queue = new LinkedList<>();
    Object lock = new Object();
    public void add(String data){
        queue.add(data);
        lock.notify();
    }
    public String take() throws InterruptedException {
        while (queue.isEmpty()){
            lock.wait();
        }
        //返回队列的头结点
        return queue.remove();
    }
}

The core idea of ​​this code is to use lock.wait() to block when the queue is empty , and then use lock.notify() to wake up when the add() method is called to add elements. This code may cause the following problems:

  • A consumer calls the take() method to get data, but queue.isEmpty() , so feeds back to the producer.
  • Before the consumer calls wait, the consumer thread is suspended due to CPU scheduling, the producer calls add(), and then notify().
  • The consumer then calls wait(). Wait is called after notify due to a wrong condition.
  • In this case, the consumer will be suspended all the time, and the producer will no longer produce, so there is a problem with this blocking queue.

From this point of view, when calling wait and notify, which will suspend operations, a synchronization mechanism is required to ensure


3. The comparison between wait and sleep

In theory, there is no comparison between wait and sleep, because wait is often used for inter-thread communication, and sleep is to block threads for a period of time. The only similarity is that both threads can give up execution for a period of time.

  • 1.wait needs to be used with the synchronized keyword, but sleep does not.
  • 2.wait is an object method, and sleep is a static method of the Thread class.
  • 3.Wait is woken up by notify is a normal business category, sleep is woken up by Interrupt and needs to report an exception.

Guess you like

Origin blog.csdn.net/liu_xuixui/article/details/128459286