This is the reading note for chapters 2 and 3 of the Art of Concurrent Programming in Java. This content has seen several times before, but easy to forget, just write it down, forget to look at, on the Internet is also convenient.

preface

Java code is compiled into Java bytecode, which is loaded by the classloader into the JVM. The JVM executes the bytecode, which ultimately needs to be converted into assembly instructions for execution on the CPU. The concurrency mechanism used in Java depends on the IMPLEMENTATION of the JVM and the INSTRUCTIONS of the CPU.

Java Memory Model (JMM)

Communication mechanism between threads

There are two mechanisms for communication between threads:

  1. Shared memory: In the concurrent model of shared memory, threads share the common state of the program and communicate implicitly through the common state in write-read memory.
  2. In the concurrent model of messaging, there is no common state between threads, and threads must communicate explicitly by sending messages.

Java memory model

Concurrency in Java is a shared memory model, where communication between Java threads is always implicit. JMM defines an abstract relationship between threads and Main Memory: shared variables between threads (instance fields, static fields, and array elements) are stored in Main Memory, and each thread has a private Local Memory where it stores copies of shared variables to read/write. The JMM abstraction is shown below:

From the figure above, the communication between thread A and thread B must go through the following two steps:

  1. Thread A flusher the updated shared variable from local memory A to main memory.
  2. Thread B goes into main memory to read the shared variable that thread A has updated before.

Taken as A whole, these two steps are essentially thread A sending messages to thread B, and this communication must go through main memory. The JMM provides Java programmers with memory visibility assurance by controlling the interaction between main memory and local memory for each thread.

Instruction reordering

When executing a program, the compiler and processor often reorder instructions to improve performance. There are three types of reordering.

  1. Compiler optimized reordering. The compiler can rearrange the execution order of statements without changing the semantics of a single-threaded program.
  2. Instruction – level parallel reordering. Modern processors use instruction-level Parallelism (ILP) to overlap multiple instructions. If there is no data dependency, the processor can change the execution order of the machine instructions corresponding to the statement.
  3. Memory system reordering. Because the processor uses caching and read/write buffers, this makes the load and store operations appear to be out of order.

The JMM’s handler reordering rules require the Java compiler to insert Memory Barriers (Intel calls them Memory fences) to prevent reordering of a particular type of handler when generating the sequence of instructions.

Happens-before rules

The JMM addresses memory visibility between operations through the concept of happens-before, which can actually be understood as a rule established by the JMM to prohibit certain types of handler reordering:

  1. Procedure order rule: For every action in a thread, happens-before any subsequent action in that thread. In the JMM, a thread is allowed to reorder as long as the result of the execution is the same as the result of the execution.
  2. The monitor lock rule: a thread is unlocked, happens-before the thread is subsequently locked.
  3. Volatile variable rule: Writes to a volatile field are happens-before subsequent reads to that volatile field.
  4. Transitivity: If A happens-before B, and B happens-before C, then A happens-before C.
  5. Start () rule: If thread A performs an operation ThreadB_start()(starts thread B), thread A’s ThreadB_start() happens before any operation in THREAD B.
  6. Join () rule: if A executes threadb.join () and returns successfully, any action in ThreadB will return successfully before thread A returns successfully from threadb.join ().
  7. Interrupt () principle: A call to the interrupt() method occurs when the interrupted Thread code detects that an interrupt event has occurred. You can use thread.interrupted () to check for interruption.
  8. Finalize () principle: The completion of an object’s initialization occurs first at the start of its Finalize () method.

volatile

Volatile is a lightweight synchronize that ensures “visibility” of shared variables in multiprocessor development. Visibility: When one thread modifies a shared variable, another thread can immediately read the changed value.

Definition of volatile: The Java programming language allows threads to access shared variables. If a field is declared volatile, the Java thread memory model ensures that all threads see the variable’s value as consistent.

How volatile is implemented: Code that shares volatile variables will generate assembly code with an extra Lock prefix.

The Lock prefix directive acts as a memory barrier that provides the following functions:

  1. Cache invalidation:
  • Causes the current CPU cache data to be written back to system memory

  • Write back to memory invalidates cache data from other cpus

  1. Disallow reordering:
    • Subsequent instructions cannot be reordered to the location in front of the memory barrier

Volatile variables have the following properties:

  1. Visibility. A read of a volatile variable always sees the last write (by any thread) to that volatile variable. Read-write of volatile variables enables communication between threads.

  2. Atomicity. Read/write to any single volatile variable is atomic, but compliance operations such as volatile ++ are not.

Volatile ++ is not atomic. Suppose thread A reads inc with A value of 10, and thread B reads inc 10 and increments and writes to main memory. However, since thread A has already read, the value of inc is still 10, and it is still 11 after incrementing, and it is written back to main memory. In this case, both threads increase() twice, but only once.

sychronized

Sychronized is the basis of Java synchronization. The correct use can ensure thread safety and solve the concurrent synchronization problem in multithreading. All Objects in Java have a mutex, which is automatically acquired and released by the JVM.

Application scenarios

There are roughly two application scenarios of sychronized:

Object lock

Object locks are used to synchronize calls to instance methods. Both of the following uses are object locks:

  1. A block of code that modifies instance methods and locks objects configured in synchronized parentheses.
        public void run() { synchronized (this) { ... }}Copy the code
  1. Modifier instance method that locks the current instance object.
        private synchronized void runInfo() {... }Copy the code
  1. Modifies static variables, locking the declaration of static variable class object instance
        private static Object object = new Object();
        @Override
        public void run() { synchronized (object) { ... }}Copy the code

Kind of lock

What a class lock does: Synchronizes the invocation of static methods or synchronizes the invocation of methods (during the “load” phase of the class’s life cycle, a java.lang.class object instance of the loaded class is generated as a method area at the entry point to the class’s various data accesses). The following two uses are class locks:

  1. Modifies the Class object, locking the Class object.
        public void run() { synchronized (Timer.class) { ... }}Copy the code
  1. Modifies static methods that lock the Class object of their Class.
        private static synchronized void runInfo() {... }Copy the code

The Demo program

The four demos share one Main function

    public static void main(String[] args) {
        Timer timer = new Timer();
        Thread thread1 = new Thread(new Timer(), "Thead-1");
        Thread thread2 = new Thread(new Timer(), "Thead-2");
        Thread thread3 = new Thread(timer, "Thead-3");
        Thread thread4 = new Thread(timer, "Thead-4");

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
Copy the code
Object lock 1: code block that decorates instance methods
    static class Timer implements Runnable {
        @Override
        public void run() {
            String threadInfo = Thread.currentThread().getName();
            System.out.println("threadInfo: Outer" + threadInfo + "Start Time," + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            synchronized (this) {
                System.out.println("threadInfo: Inner" + threadInfo + "Start Time," + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("threadInfo:" + threadInfo + "End Time," + new SimpleDateFormat("HH:mm:ss").format(new Date())); }}}Copy the code
  1. Thread-1,2, and 3 are three independent object instances, so the start and end times are the same. Thread3 and 4 share an object instance. The object lock guarantees that only one thread holding the object lock can access the synchronized code block at a time.
  2. An area outside of a synchronized code block where individual threads can execute concurrently.
ThreadInfo: outerthead-4, Start Time:20:04:29 threadInfo: outerthead-2, Start Time:20:04:29 threadInfo: outerthead-4, Start Time:20:04:29 Outerthead-3, Start Time:20:04:29 threadInfo: outerthead-1, Start Time:20:04:29 threadInfo: Innerthead-4, Start Time:20:04:29 threadInfo: innerthead-2, Start Time:20:04:29 threadInfo: innerthead-2, Start Time:20:04:29 threadInfo: Innerthead-1, Start Time:20:04:29 threadInfo: thead-4, End Time:20:04:34 threadInfo: thead-2, End Time:20:04:34 threadInfo: thead-1, End Time:20:04:34 threadInfo: Innerthead-3, Start Time:20:04:34 threadInfo: theAD-3, End Time:20:04:39Copy the code
Object lock 2: Decorates instance methods
    static class Timer implements Runnable {
        @Override
        public synchronized void run() {
            String threadInfo = Thread.currentThread().getName();
            System.out.println("threadInfo: Outer" + threadInfo + "Start Time," + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            System.out.println("threadInfo: Inner" + threadInfo + "Start Time," + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("threadInfo:" + threadInfo + "End Time," + new SimpleDateFormat("HH:mm:ss").format(new Date())); }}Copy the code
  1. The three object instances of thread-1,2, and 3 are independent. Therefore, three threads are executed concurrently, so the start and end times are the same. Thread3 and thread4 share an object instance, and the synchronized methods are executed sequentially by Thread3 and thread4.
  2. The synchronization scope is the entire method.
ThreadInfo: outerthead-1, Start Time:20:12:00 threadInfo: outerthead-3, Start Time:20:12:00 threadInfo: Outerthead-2, Start Time:20:12:00 threadInfo: innerthead-1, Start Time:20:12:00 threadInfo: Innerthead-3, Start Time:20:12:00 threadInfo: Innerthead-2, Start Time:20:12:00 threadInfo: thead-1, End Time:20:12:05 threadInfo: thead-3, End Time:20:12:05 threadInfo: thead-2, End Time:20:12:05 threadInfo: outerthead-4, Start Time:20:12:05 threadInfo: Innerthead-4, Start Time:20:12:05 threadInfo: thead-4, End Time:20:12:10Copy the code
Object lock 3: Modifies static variables
    static class Timer implements Runnable {
        private static Object object = new Object();
        @Override
        public void run() {
            String threadInfo = Thread.currentThread().getName();
            System.out.println("threadInfo: Outer" + threadInfo + "Start Time," + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            synchronized (object) {
                System.out.println("threadInfo: Inner" + threadInfo + "Start Time," + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("threadInfo:" + threadInfo + "End Time," + new SimpleDateFormat("HH:mm:ss").format(new Date())); }}}Copy the code
  1. The object lock here is a bit special, because there is only one instance object of a static variable, so four threads need to queue up to execute a synchronized code block when they encounter a synchronized block. Whoever holds the lock executes it, and when the lock is finished, the lock is released, and the next thread acquires the lock and executes it.
  2. An area outside of a synchronized code block where individual threads can execute concurrently.
ThreadInfo: outerthead-4, Start Time:15:36:45 threadInfo: outerthead-3, Start Time:15:36:45 threadInfo: Outerthead-2, Start Time:15:36:45 threadInfo: outerthead-1, Start Time:15:36:45 threadInfo: Innerthead-4, Start Time:15:36:45 threadInfo: thead-4, End Time:15:36:50 threadInfo: Innerthead-1, Start Time:15:36:50 threadInfo: thead-1, End Time:15:36:55 threadInfo: Innerthead-2, Start Time:15:36:55 threadInfo: thead-2, End Time:15:37:00 threadInfo: Innerthead-3, Start Time:15:37:00 threadInfo: thead-3, End Time:15:37:05Copy the code
Class lock 1: Decorates the Class object
    static class Timer implements Runnable {
        @Override
        public void run() {
            String threadInfo = Thread.currentThread().getName();
            System.out.println("threadInfo: Outer" + threadInfo + "Start Time," + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            synchronized (Timer.class) {
                System.out.println("threadInfo: Inner" + threadInfo + "Start Time," + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("threadInfo:" + threadInfo + "End Time," + new SimpleDateFormat("HH:mm:ss").format(new Date())); }}}Copy the code
  1. A class lock guarantees that only one thread holding the class lock can access a synchronized code block at a time.
  2. An area outside of a synchronized code block where individual threads can execute concurrently.
  3. The lock is the Class object of the Class, java.lang.class.
ThreadInfo: outerthead-2, Start Time:20:17:46 threadInfo: outerthead-1, Start Time:20:17:46 threadInfo: outerthead-2, Start Time:20:17:46 Outerthead-3, Start Time:20:17:46 threadInfo: outerthead-4, Start Time:20:17:46 threadInfo: Innerthead-2, Start Time:20:17:46 threadInfo: thead-2, End Time:20:17:51 threadInfo: Innerthead-4, Start Time:20:17:51 threadInfo: thead-4, End Time:20:17:56 threadInfo: Innerthead-3, Start Time:20:17:56 threadInfo: thead-3, End Time:20:18:01 threadInfo: Innerthead-1, Start Time:20:18:01 threadInfo: thead-1, End Time:20:18:06Copy the code
Class lock 2: Decorates static methods
    static class Timer implements Runnable {
        @Override
        public void run() {
            runInfo();
        }

        private static synchronized void runInfo() {
            String threadInfo = Thread.currentThread().getName();
            System.out.println("threadInfo: Outer" + threadInfo + "Start Time," + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            System.out.println("threadInfo: Inner" + threadInfo + "Start Time," + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("threadInfo:" + threadInfo + "End Time," + new SimpleDateFormat("HH:mm:ss").format(new Date())); }}Copy the code
  1. A class lock guarantees that only one thread holding the class lock can access a synchronized code block at a time.
  2. The scope of class lock synchronization that modifies static methods is the entire static method.
  3. The lock is the Class object of the Class, java.lang.class.
ThreadInfo: outerthead-1, Start Time:20:27:15 threadInfo: Innerthead-1, Start Time:20:27:15 threadInfo: thead-1, End Time:20:27:20 threadInfo: Outerthead-3, Start Time:20:27:20 threadInfo: Innerthead-3, Start Time:20:27:20 threadInfo: thead-3, End Time:20:27:25 threadInfo: Outerthead-4, Start Time:20:27:25 threadInfo: Innerthead-4, Start Time:20:27:25 threadInfo: thead-4, End Time:20:27:30 threadInfo: Outerthead-2, Start Time:20:27:30 threadInfo: innerthead-2, Start Time:20:27:30 threadInfo: thead-2, End Time:20:27:35Copy the code

The realization principle of synchronized

The JVM implements method synchronization and code block synchronization based on entering and exiting monitor objects.

Block synchronization

For code block synchronization:

  1. After compilation, the JVM inserts the MonitoRenter directive at the start of the synchronized code block and the Monitorexit directive at the end of the method and the exception. The JVM guarantees that each Monitorenter must have a Monitorexit that matches it.
  2. Any object has a Monitor object associated with it (a pointer to the mutex monitor is stored in the Mark Word of the object header), and when a Monitor object is held, it is locked.
  3. When the thread executes the Monitorenter instruction, it attempts to acquire the ownership of the object’s monitor, i.e., the lock on the object, as follows:
  • If monitor’s number of entries is 0, the thread enters Monitor, then sets the number of entries to 1, and the thread is the owner of Monitor.
  • If the thread already owns the monitor and simply re-enters, the number of entries into the monitor is increased by one (reentrant).
  • If another thread has occupied monitor, the thread blocks until the number of monitor entries is zero, and then tries again to acquire ownership of monitor.

Methods synchronous

For method synchronization:

  1. The JVM puts it in the constant pool of synchronized methods after compilationACC_SYNCHRONIZEDMark.
  2. When a thread accesses a method, it first checks to see if it existsACC_SYNCHRONIZEDIf set, attempts will be made to obtain ownership of the monitor corresponding to the object. Get before executing the method.
  3. If an exception occurs during method execution and is not handled internally, the monitor lock is automatically released before the exception is thrown outside the method.

In fact, the “lock record” is stored in Java objects in both object locks and class locks, except that the lock record of class locks is stored in object instances of java.lang.class, and the lock record of object locks is stored in object instances of other classes. Which brings us to the following.

Memory layout of Java objects

In the HotSpot virtual machine, the layout of objects stored in memory can be divided into three areas: object headers, Instance Data, and alignment Padding.

Object head

In the case of the JDK8 64-bit HotSpot VIRTUAL machine, the object header contains three pieces of information:

  1. Mark Word: Stores runtime data of the object itself, such as HashCode, GC generation age, lock status flags, locks held by threads, bias thread IDS, bias timestamps, etc. The size is 8 bytes.

  2. Class Word: A type pointer to an object’s Class metadata that the virtual machine uses to determine which Class the object is an instance of. The size is 4 bytes in JDK 8 with pointer compression enabled by default.

  3. Array Length: The header contains this section only if the object is an instance of a Java Array. Store the length of the array. The size is 4 bytes.

The instance data

The instance data portion is the valid information that the object actually stores, as well as the content of various types of fields defined in program code. Both inherited from a parent class and defined in a subclass need to be logged.

Alignment filling

Alignment padding may exist and serves only as a placeholder. HotSpot VM requires that the object’s size be an integer multiple of 8 bytes.

From the memory layout of the Java object, we can see that the lock information is stored in the Mark of the Java object header.

Optimization of synchronized locks

Prior to JDK 1.6, synchronized locks were heavyweight locks, implemented with the mutex monitor. Starting with JDK 1.6, the HotSpot VIRTUAL Machine team introduced lock optimizations such as adaptive spin, lightweight locking, and biased locking. These lock optimizations are at the discretion of the virtual machine based on competition and are encapsulated in the synchronized implementation.

Blocking and waking up a thread requires the CPU to move from user mode to core mode, and in many cases synchronized locking only lasts for a short period of time. It is not worth blocking and waking up a thread frequently for this short period of time. So spin locks.

Kernel mode: The CPU can access all data in memory, including peripheral devices such as hard disks and network cards. The CPU can also switch itself from one program to another.

User mode: Memory access is limited and peripheral device access is not allowed. CPU usage is deprived and CPU resources can be obtained by other programs.

The author of HotSpot found through research that in most cases, locks are not only not contested by multiple threads, but are always acquired by the same thread multiple times, and biased locks are introduced to make the cost of lock acquisition lower.

Biased locking

Lock-biased locking, when a thread enters and exits a synchronized block, requires several testing steps:

  1. When a thread accesses a block and acquires a lock, the thread ID of the lock bias is stored in the lock record in the object header and stack frame.
  2. The thread does not need to perform CAS operations to lock and unlock the synchronized block when it enters and exits the block. It simply tests whether the object header’s Mark Word stores biased locks to the current thread.
  3. If the test succeeds, the thread has acquired the lock. If the test fails, you need to test whether the Mark of biased lock is set to 1 (indicating that it is currently a biased lock) in The Mark Word. If not, CAS is used to compete for the lock. If set, try using CAS to point the bias lock of the object header to the current thread.

When other threads try to compete for the biased lock, the lock will be released. The lock cancellation needs to wait for the global safety point, which is divided into the following steps:

  1. Suspends a thread with a bias lock to check whether the thread is alive

  2. If it is inactive, set it to lock – free

  3. If it survives, it will revert to another thread or return to a lock-free state or mark object unsuitable as a biased lock (upgraded to lightweight lock)

  4. Wake up the thread

Lightweight lock

Lightweight locking:

  1. The JVM will create a space for storing lock records in the current thread’s stack frame before executing the synchronized block and copy the product header Mark Word into the lock record.
  2. The thread then tries to use CAS to replace the Mark Word in the object header with a pointer to the lock record.
  3. If successful, the current thread acquires the lock; If this fails, other threads compete for the lock, and the current thread attempts to acquire the lock using spin.

Lightweight lock unlock:

  1. Lightweight unlocking uses atomic CAS operation to replace the Product of the Taliban Mark Word back to the object.

  2. If successful, no competition has occurred. If it fails, it indicates that the current lock is competing, and the lock expands to a heavyweight lock.

Reference and thanks

  1. www.cnblogs.com/uoar/p/1167…
  2. Juejin. Cn/post / 684490…
  3. Juejin. Cn/post / 684490…
  4. Blog.csdn.net/carson_ho/a…
  5. Segmentfault.com/a/119000000…