1, Re-learn Java Basic Thread Foundation (2)

1.1, volatile

1.1.1 Basic Concepts

The Java memory model specifies that all variables are stored in main memory, where main memory is only a part of the virtual machine memory, and virtual machine memory is only a part of the physical computer memory (the part allocated for the virtual machine process).

The Java memory model is divided into main memory and working memory. Main memory is shared by all threads, while working memory is shared by each thread.

Each thread also has its own working memory, which holds a copy of the main memory of the variables used by the thread. All operations on variables (reading, assigning) must be performed in working memory, not directly reading or writing variables in main memory. Different threads cannot directly access variables in each other’s working memory, and the transfer of variable values between threads needs to be completed through main memory.

Read-load Copies variables from the main memory to the current working memory, use-assign executes code to change the value of the shared variable, and store-write refreshes main memory with the working memory data.

Java interacts working memory with main memory through several atomic operations:

  • Lock: applies to main memory to identify variables as thread-exclusive.
  • Unlock: The main memory is unmonopolized.
  • Read: Transfers the value of a variable from main memory to the thread’s working memory.
  • Load: Acts on working memory to place the value of the variable passed by the read operation into a copy of the variable in working memory.
  • Use: Functions as working memory, passing the value of a variable in working memory to the execution engine.
  • Assign: For working memory. Assign a value received from the execution engine to a variable in working memory.
  • Store: a variable that acts on working memory, transferring the value of a variable in working memory to main memory.
  • Write: Main memory variable. The value of the variable passed from the store operation is placed in the main memory variable.

The atomic variable operations directly guaranteed by the Java memory model include read, Load, use, assign, Store and write. It can be generally considered that the access and read and write of the basic data types are atomic. The Java memory model also provides lock and unlock operations if a wider range of atomicity guarantees is required, although the virtual machine does not make them available directly to users. But it provides higher-level bytecode instructions monitorenter and monitorexit to implicitly use these two operations. These bytecode instructions are reflected in Java code as synchronized blocks — the synchronized keyword, Thus, operations between synchronized blocks are also atomic.

JVM constraints on interactive instructions

To copy a variable from main memory to working memory, the read and load operations are performed sequentially, and to synchronize the variable from working memory back to main memory, the store and write operations are performed sequentially.

The Java memory model only requires that the above operations be performed sequentially; there is no guarantee that they must be performed consecutively. Between read and Load, between store and write, other instructions can be inserted. For example, when accessing variables a and b in main memory, the possible order is read a, read b, load B, load a. The Java memory model also specifies that the following rules must be met when performing the eight basic operations described above:

  • One of the read and load, store, and write operations is not allowed separately

  • A thread is not allowed to discard its most recent assign operation, that is, variables changed in working memory must be synchronized to main memory.

  • A thread is not allowed to synchronize data from working memory back to main memory for no reason (no assign operation has occurred).

  • A new variable can only be created in main memory. It is not allowed to use an uninitialized (load or assign) variable in working memory. The use and store operations must be performed before the assign and load operations can be performed.

  • Only one thread can lock a variable at a time. Lock and unlock must be paired

  • If you lock a variable, the value of the variable will be cleared from the working memory, and you will need to load or assign the variable again to initialize it before the execution engine can use it

  • Unlock is not allowed if a variable has not been locked by the lock operation. It is also not allowed to unlock a variable that is locked by another thread.

  • Before you can unlock a variable, you must synchronize it to main memory (store and write).

1.1.2,

Memory visibility is ensured so that all threads can see the latest status of shared memory.

The special rule for volatile is:

  • The read, load, and use actions must occur consecutively.
  • The assign, Store, and write actions must occur consecutively.

Therefore, using volatile variables ensures that:

  • The latest value must be flushed from main memory before each read.
  • Each write must be synchronized back to main memory immediately.

Prevents reordering of instructions

Instruction reordering is to optimize instructions, improve program running efficiency, and improve parallelism as much as possible under the premise of not affecting the execution results of single-threaded programs. Instruction reordering includes compiler reordering and runtime reordering.

The classic problem is lazy singleton loading and Double Check Lock (DCL).

How does volatile prevent order reordering

The volatile keyword uses a “memory barrier” to prevent instructions from being reordered.

To implement the memory semantics of volatile, the compiler inserts a memory barrier into the instruction sequence when generating bytecode to prohibit reordering of certain types of handlers. However, it is almost impossible for the compiler to find an optimal placement to minimize the total number of insertion barriers, and the Java memory model takes a conservative approach to this end.

The following is a conservative policy based JMM memory barrier insertion strategy:

  • Insert a StoreStore barrier before each volatile write.
  • Insert a StoreLoad barrier after each volatile write.
  • Insert a LoadLoad barrier after each volatile read.
  • Insert a LoadStore barrier after each volatile read operation
1.1.3 volatile Traps

Volatile makes reads and writes to a variable “atomic” in relation to each other, but not atomic when the change in the value of the variable is caused by its own value. Atomicity works if it is caused by the value of another variable. Such as:

Public static volatile int n = 0; n = n + 1; n++; # Atomicity n = m + 1Copy the code

In fact, volatile does not guarantee that all cases will not be reordered. For example, in two cases, reordering is allowed:

Read/write operations on ordinary variables, and then read operations on volatile variables

2. Write operations on volatile variables followed by read/write operations on ordinary variables

1.2. What are the problems caused by multithreading and how to solve them

Deadlocks can occur if the thread does not operate properly.

Multiple threads are blocked at the same time. One or all of them are waiting for a resource to be released, which is locked by another thread. As a result, each thread has to wait for the other thread to release its locked resource.

The necessary conditions for creating a deadlock are:

  • 1. Mutual exclusion: when a resource is used (owned) by one thread, other threads cannot use it.
  • 2. Inalienable conditions: Resource requesters cannot forcibly seize resources from resource occupiers, and resources can only be released by resource occupiers on their own initiative.
  • 3, request and hold conditions: the thread has maintained at least one resource, but made a new resource request, and the resource has been occupied by another thread, at this time the request thread is blocked, but it has to hold the resource.
  • 4, circular wait condition: there is a circular wait chain of thread resources, each thread in the chain has obtained resources at the same time by the next thread in the chain request.

When all four conditions are true, a deadlock is formed. Of course, if any of these conditions are broken in the case of a deadlock, the deadlock will disappear.

1.3 Thread communication

Sometimes we need multiple threads to cooperate to complete a task, and since cooperation is necessary, there must be communication between threads. Let’s try to see how multiple threads can collaborate to complete a task.

There is now a requirement for two threads to read data from a collection. For example, if thread A reads the first element, thread B reads the second element.

The first attempt:

public class TestThread {

    public static List<Integer> numList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

    public static int point = 0;

    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        Thread2 thread2 = new Thread2();

        thread1.start();
        thread2.start();

    }


    public static class Thread1 extends Thread {

        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println(String.format("%s:%d",Thread.currentThread().getName(),TestThread.numList.get(TestThread.point)));
                    TestThread.point++;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static class Thread2 extends Thread {

        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println(String.format("%s:%d",Thread.currentThread().getName(),TestThread.numList.get(TestThread.point)));
                    TestThread.point++;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

# 结果

Thread-0:1
Thread-1:1
Thread-0:3
Thread-1:3
Thread-1:5
Thread-0:5
Thread-1:7
Thread-0:7
Thread-1:9
Thread-0:9
Copy the code

The data read by the two threads should not be repeated, but we found that the data read by the two threads was repeated because our increments were not synchronized. Let’s try the next step:

public static class Thread1 extends Thread { @Override public void run() { for (int i = 0; i < 5; i++) { try { synchronized(TestThread.class){ System.out.println(String.format("%s:%d",Thread.currentThread().getName(),TestThread.numList.get(TestThread.point))); TestThread.point++; Thread.sleep(500); } } catch (InterruptedException e) { e.printStackTrace(); }}}} # Thread2 # Result thread-0:1 thread-0:2 thread-0:3 thread-1:4 thread-0:5 thread-0:6 thread-1:7 thread-1:8 Thread-1:9 Thread-1:10Copy the code

The numbers are continuous, but the threads do not cross.

Then we try again:

public class TestThread {

    public static List<Integer> numList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

    public static int point = 0;

    public static boolean read = false;

    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        Thread2 thread2 = new Thread2();

        thread1.start();
        thread2.start();

    }


    public static class Thread1 extends Thread {

        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                try {
                    synchronized(TestThread.class){
                        if (!TestThread.read){
                            TestThread.read = true;
                            TestThread.class.notify();
                        }
                        System.out.println(String.format("%s:%d",Thread.currentThread().getName(),TestThread.numList.get(TestThread.point)));
                        TestThread.point++;
						Thread.sleep(500);
                        if (TestThread.point == 10){
                            TestThread.class.notifyAll();
                        }else{
                            TestThread.class.wait();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static class Thread2 extends Thread {

        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                try {
                    synchronized(TestThread.class){
                        if (TestThread.read){
                            TestThread.read = false;
                            TestThread.class.notify();
                        }
                        System.out.println(String.format("%s:%d",Thread.currentThread().getName(),TestThread.numList.get(TestThread.point)));
                        TestThread.point++;
						Thread.sleep(500);
                        if (TestThread.point == 10){
                            TestThread.class.notifyAll();
                        }else{
                            TestThread.class.wait();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

# 打印

Thread-0:1
Thread-1:2
Thread-0:3
Thread-1:4
Thread-0:5
Thread-1:6
Thread-0:7
Thread-1:8
Thread-0:9
Thread-1:10
Copy the code

Ok, now it fits our needs. Let’s examine this code:

1. Synchronized ()

If thread A locks an instance of Thread1 and thread B locks an instance of Thread2, the two threads can acquire the lock object (the object monitor) without any obstruction, and there is no synchronization effect. With the testthread. class lock, thread A will acquire the synchronization lock. Thread B will block access to the synchronization method because it cannot acquire the synchronization lock.

Testthread.class.wait ()

Notify and wait to perform in the synchronization method, otherwise will be thrown IllegalMonitorStateException anomalies. Incorrect releases the lock will throw IllegalMonitorStateException abnormalities, such as no locking an object and USES wait to release the lock will throw an exception. Methods such as wait are called by the object lock. Whoever locks the object can release the lock.

Testthread.class.wait ()

Following the previous problem, after synchronization, each thread may execute several times due to various factors that cause the priority of the thread to be different after completing the loop, so a wait is placed later to stop the thread after completing the loop.

4, why do you need to add TestThread. Class. NotifyAll ()?

If you do not add a notifyAll, the program will continue to pause. If you do not add a notifyAll, the notify process will continue. If you do not add a notifyAll, the notify process will continue. Although notify wakes up a thread in a wait at random, since there are only two threads and one thread has already been awakened, using notify must wake up the only thread left. NotifyAll wakes up all threads in a WAIT.

5. Why does the result warrant cross-execution?

Testthread.class.notify () randomly wakes up the thread and releases the lock. After releasing the lock, the notify code is not synchronized, so it is easy for the thread to block permanently. This is where notify releases locks. The notify is to release the lock, but not immediately. It is to wait until the synchronization block is finished (I won’t test this conclusion), thus ensuring the execution of the current thread’s wait.

There is also a wait(long) method with an argument, which wakes it up if a thread wakes it up within a specified period of time, or wakes it up automatically if not. The unit is ms.

Can interrupt interrupt a wait

Don’t we know how to interrupt? Does calling interrupt while the thread is in wait terminate the thread? The answer is yes.

public class TestThread { public static void main(String[] args) { Thread1 thread1 = new Thread1(); thread1.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } thread1.interrupt(); } public static class Thread1 extends Thread { @Override public void run() { synchronized (this){ try { System.out.println(" I'm running "); this.wait(); System.out.println(" I'm done "); } catch (InterruptedException e) { e.printStackTrace(); } } } } }Copy the code

After 1S thread1 throws an InterruptedException.

Can interrupt terminate a blocking thread?

Can interrupt terminate a blocking thread? The answer is that interrupts can only terminate non-blocking threads.

public class TestThread { public static void main(String[] args) { Thread1 thread1 = new Thread1(); Thread1 thread2 = new Thread1(); thread1.start(); thread2.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } thread2.interrupt(); System.out.println(" Interrupt execution "); } public static class Thread1 extends Thread { @Override public void run() { synchronized (TestThread.class){ try { System.out.println(Thread.currentThread().getName()); Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } } } }Copy the code

If an interrupt can terminate a blocking thread, then executing thread2.interrupt(); But it doesn’t. The exception is thrown just the same, but after the interrupt is finished, thread 2 waits for thread 1 to finish executing before raising the exception. To terminate a blocking thread, you must use the WAIT method to release the lock so that the blocking thread has a chance to acquire the lock and continue executing.

1.4, the join

Follow-up……

1.5, ThreadLocal

ThreadLocal allows each thread to have its own “little secret.” Per-thread ThreadLocal data is isolated with respect to each thread.

public class TestThread { public static ThreadLocal<Integer> local = new ThreadLocal<>(); public static void main(String[] args) { Thread thread = new Thread(() -> { String name = Thread.currentThread().getName(); System.out.println(name + "set value 666"); local.set(666); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(name+local.get()); }); Thread thread1 = new Thread(() -> { String name = Thread.currentThread().getName(); System.out.println(name + "set value 777"); local.set(777); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(name+local.get()); }); thread.start(); thread1.start(); }} # Set the value of thread-0 to 666 thread-1 to 777 thread-0666 thread-1777Copy the code

Thread 1 sets the value to sleep for 2 seconds, and thread 2 executes the value. Even though local is a global static variable, the value printed in thread is the same as the value set by thread 1.

How do you set the default value for ThreadLocal?

public class TestThread { public static ThreadLocal local = new TestThreadLocal(); public static void main(String[] args) { System.out.println(local.get()); } public static class TestThreadLocal extends ThreadLocal{ @Override protected Object initialValue() { return "688"; }}}Copy the code

Inheriting ThreadLocal and overriding initialValue overrides the default value of null.

Subsequent small details will be updated in the source address: www.nblogs.cn/doc/java/b1…

Reference address:

  • www.cnblogs.com/jpfss/p/995… Java- Threaded (working memory) and memory model (main memory) analysis
  • Blog.csdn.net/y874961524/… Java thread communication memory model – main memory and working memory
  • www.cnblogs.com/monkeysayhi… The function and principle of volatile keyword