[JavaEE Elementary] Multithreading (3) volatile wait notify keyword singleton mode

Photography sharing~~
insert image description here

volatile keyword

volatile can guarantee memory visibility

import java.util.Scanner;

class MyCounter {
    
    
    public int flag = 0;
}

public class ThreadDemo14 {
    
    
    public static void main(String[] args) {
    
    
        MyCounter myCounter = new MyCounter();

        Thread t1 = new Thread(() -> {
    
    
            while (myCounter.flag == 0) {
    
    
                try {
    
    
                    Thread.sleep(100);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
            System.out.println("t1 循环结束");
        });

        Thread t2 = new Thread(() -> {
    
    
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数: ");
            myCounter.flag = scanner.nextInt();
        });

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

The result of running the above code may be that after inputting 1, the thread t1 does not end. Instead, it keeps looping in while. And the t2 thread has been executed.
The above situation is called the problem of memory visibility.
insert image description here
Here we use assembly to understand it, which is roughly divided into two steps:

  1. load, read the value of the flag in the memory into the register.
  2. cmp, compare the value in the register with 0. According to the comparison result, decide where to execute the next step (conditional jump instruction)

The loop body of the above loop is empty, and the loop execution speed is extremely fast. The loop is executed many times, and the result obtained by load is the same until t2 is actually modified. On the other hand, the load operation is much slower than the cmp operation. Because the execution speed of load is too slow (compared to cmp), and the result of repeated load is the same, JVM made a bold decision: no longer really repeat load, and it is determined that no one modifies the flag value ( But in fact, someone is modifying it, t2 is modifying), just read it once. (A way of compiler optimization)
Memory visibility problem : One thread reads a variable while another thread modifies it. The value read at this time is not necessarily the value after modification. (The jvm/compiler made a misjudgment when optimizing in a multi-threaded environment)
At this point, we need manual intervention. We can add the volatile keyword to the flag variable. Tell the compiler that this variable is "volatile" and needs to re-read the contents of this variable every time.
insert image description here
Volatile does not guarantee atomicity, atomicity is guaranteed by synchronized.

wait and notify

For example:
t1, t2 two threads, hope that t1 executes the task first, let t2 do it when the task execution is almost over, then let t2 wait first (block, give up cpu actively). When the execution of the t1 task is almost over, notify t2 through notify, wake up t2, and let t2 start executing the task.
In the above scenario, is it possible to use join and sleep?
To use join, t1 must be completely executed before t2 can be executed. If you want t1 to execute half of the tasks and then let t2 execute, the join cannot be completed.
To use sleep, a sleep time must be specified, but the time for t1 to execute tasks is difficult to estimate.
Using wait and notify can solve the above problems.

wait

Wait is blocked, a thread calls the wait method, it will enter the block, and it is in WAITING at this time.
insert image description here

This exception is carried by many methods with blocking functions, and these methods can be awakened by the interrupt method through the above exception.
Let's look at another code:

public class ThreadDemo17 {
    
    
        public static void main(String[] args) throws InterruptedException {
    
    
            Thread t = new Thread(() -> {
    
    
                try {
    
    
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println("执行完毕!");
            });

            t.start();
            System.out.println("wait前");
            t.wait();
            System.out.println("wait后");
        }
}

insert image description here
An illegal lock state exception occurs here. The state of the lock is generally the locked state and the unlocked state.
Why does this exception occur? It is related to the operation of wait:

The operation of wait:

  1. release the lock first
  2. block wait
  3. After receiving the notification, try to acquire the lock again, and continue to execute after acquiring the lock.

The above code wants to release the lock without the lock, so an illegal lock state exception occurs.
Therefore, the wait operation should be used with synchronized.
insert image description here

notify

wait and notify are generally used together. The notify method is used to wake up the thread waiting for wait, wait can release the lock and make the thread wait, and notify can acquire the lock after waking up the thread, and then make the thread continue to execute.
insert image description here
If in the above code, t1 has not executed wait, and t2 has executed notify, then the statement at this time is useless. After t2 executes notify, t1 will block and wait after executing wait.
Note that after the above code wakes up t1 at t2, the execution between t1 and t2 is random, and the order of the places labeled 3 and 4 is uncertain.

method Effect
wait(); No parameters, wait until notify wakes up
wait(time parameter); Specify maximum wait time

notifyAll

The notify method just wakes up a certain waiting thread. Use the notifyAll method to wake up all waiting threads at once.
In general, use notify. Because all wake-ups will lead to preemptive execution between threads. Not necessarily safe.

The difference between wait and sleep

Same point:

  • It is possible to suspend threads for a period of time to control the execution order between threads.
  • wait can set a maximum waiting time, and sleep can be woken up in advance.

difference:

  • wait is a method in the Object class, and sleep is a method in the Thread class.
  • wait must be used in a synchronized modified code block or method, and the sleep method can be used anywhere.
  • After wait is called, the current thread enters the BLOCK state and releases the lock, and can be woken up by notify and notifyAll methods; after sleep is called, the current thread enters the TIMED_WAITING state, which does not involve lock-related operations.
  • Using sleep can only specify a fixed sleep time, and the execution time of the operation in the thread cannot be determined; while using wait can wake up the thread at the specified operation position.
  • Both sleep and wait can be woken up in advance. Interrupt wakes up sleep, and an exception will be reported. This method is an abnormal execution logic; while noitify wakes up wait is normal business execution logic, and there will be no exceptions.

little practice

There are three threads, which can only print A, B, and C respectively. Control the three threads to print in the order of ABC.

public class ThreadDemo18 {
    
    
    // 有三个线程, 分别只能打印 A, B, C. 控制三个线程固定按照 ABC 的顺序来打印.
    public static void main(String[] args) throws InterruptedException {
    
    
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
    
    
            System.out.println("A");
            synchronized (locker1) {
    
    
                locker1.notify();
            }
        });
        Thread t2 = new Thread(()->{
    
    
            synchronized (locker1) {
    
    
                try {
    
    
                    locker1.wait();
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }

            }
            System.out.println("B");
            synchronized (locker2) {
    
    
                    locker2.notify();
            }
        });
        Thread t3 = new Thread(()->{
    
    
            synchronized (locker2) {
    
    
                try {
    
    
                    locker2.wait();
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
            }
            System.out.println("C");
        });
        t2.start();
        t3.start();
        Thread.sleep(100);
        t1.start();
    }
}

Create locker1 for use by 1 and 2
Create locker2 for use by 2 and
3 Thread 3, locker2.wait()
thread 2, locker1.wait() wakes up and execute locker2.notify
thread 1 to perform its own task, after execution locker. notify
insert image description here

Multi-threaded case

singleton pattern

The singleton pattern is a type of design pattern.
The singleton mode can ensure that there is only one instance of a certain class in the program, and multiple instances will not be created.
The specific implementation methods of the singleton mode are divided into "hungry man" and "lazy man".

hungry man mode

When the class is loaded, an instance is created.
There is only one copy of a class object in a java process. Therefore, the class attribute inside the class object is also unique.
In the class loading phase, the instance is created.

//饿汉模式的单例模式的实现
//保证Singleton这个类只能创建出一个实例
class Singleton{
    
    
    //在此处,先将实例创建出来
    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
    
    
        return instance;
    }
    //为了避免Singleton类不小心被多复制出来
    //把构造方法设为private,在类外,无法通过new的方式来创建一个Singleton
    private Singleton(){
    
    

    }
}
public class ThreadDemo19 {
    
    
    public static void main(String[] args) {
    
    
        Singleton s = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        //Singleton s3 = new Singleton();
        System.out.println(s == s2);
    }
}

insert image description here
insert image description here

  • static guarantees that this instance is unique
  • static ensures that this instance is created.

lazy mode

class SingletonLazy{
    
    
    private static SingletonLazy instance = null;


    public static SingletonLazy getIsntance() {
    
    
        if(instance == null){
    
    
            instance = new SingletonLazy();
        }
        return instance;
    }
}
public class ThreadDemo20 {
    
    
        public static void main(String[] args) {
    
    
            SingletonLazy s = SingletonLazy.getIsntance();
            SingletonLazy s2 = SingletonLazy.getIsntance();
            System.out.println(s == s2);
        }
}

insert image description here

Call instance in multi-thread, starving mode is not thread-safe.
insert image description here
So how to ensure the lazy man mode thread safety?
** Locked. **The essential problem of thread safety is that the three operations of reading, comparing and writing are not atomic. So we can add locks to solve thread safety issues.
insert image description here

However, the locking operation requires a certain amount of overhead for each call to getInstance. And our locking is only for before the new object, so we can judge whether the object is created, and then decide to lock it.
If the object is created, it is not locked. If the object has not been created, lock it.
insert image description here
There is still a problem with the above code, that is, the memory visibility problem:
if there are many threads calling getInstance, the code may be optimized at this time (the memory is read for the first time, and the register/cache is read later). In addition, it is
possible Instruction reordering is also involved.
insert image description here
In the above code, there are three steps:

  1. Apply for memory space
  2. Call the constructor to initialize this memory space into an object
  3. Assign the address of the memory space to the instance reference

The compiler's instruction reordering operation will adjust the code execution order, and 123 may become 132. (No effect in single thread)
We can add volatile to the code.
volatile has two functions:

  1. fix memory visibility
  2. Disable instruction reordering.

The following is the complete code of the singleton mode of the lazy man mode:

class SingletonLazy{
    
    
    private volatile static SingletonLazy instance = null;


    public static SingletonLazy getInstance() {
    
    
        if(instance ==null){
    
    
            synchronized (SingletonLazy.class){
    
    
                if(instance == null){
    
    
                    instance = new SingletonLazy();
                }
            }
        }  

        return instance;
    }
}
public class ThreadDemo20 {
    
    
        public static void main(String[] args) {
    
    
            SingletonLazy s = SingletonLazy.getInstance();
            SingletonLazy s2 = SingletonLazy.getInstance();
            System.out.println(s == s2);
        }
}

Guess you like

Origin blog.csdn.net/qq_61138087/article/details/130301126