volatile

JMM

The JMM (Java Memory Model) defines how the Java Virtual Machine (JVM) works in computer memory (RAM) and is an abstract concept. JVM design takes into account that if JAVA threads directly manipulate main memory each time they read and write variables, performance will be greatly affected. Therefore, each thread has its own working memory. Variables in working memory are a copy of main memory, and the thread reads and writes variables directly in working memory. You can’t directly manipulate variables in main memory. The problem with this, however, is that when a thread changes a variable in its own working memory, it is not visible to other threads, leading to thread-unsafe problems. So the JMM developed a set of standards to ensure that developers writing multithreaded programs can control when memory is synchronized to other threads.

Memory interoperation

There are eight types of memory interactions, and the virtual machine implementation must ensure that each operation is atomic and non-divisible. (For double and long variables, exceptions are allowed for load, store, read, and write on some platforms.)

  • Lock: A variable that acts on main memory, marking a variable as thread-exclusive
  • Unlock: A variable that acts on main memory. It releases a locked variable so that it can be locked by another thread
  • Read: Acts on a main memory variable that transfers the value of a variable from main memory to the thread’s working memory for subsequent load action
  • Load: Variable acting on working memory, which puts a read operation variable from main memory into working memory
  • Use: Applies to variables in working memory. It transfers variables in working memory to the execution engine. This instruction is used whenever the virtual machine reaches a value that requires the variable to be used
  • Assign: A variable applied to working memory that places a value received from the execution engine into a copy of the variable in working memory
  • Store: Acts on a variable in main memory. It transfers a value from a variable in working memory to main memory for subsequent writes
  • Write: A variable in main memory that puts the value of a variable in main memory from the store operation in working memory

The JMM lays down the following rules for the use of these eight directives:

  • One of the read and load, store and write operations is not allowed to occur separately. If you use read, you must load; if you use store, you must write
  • A thread is not allowed to discard its most recent assign operation, in which it must inform main memory that the work variable’s data has changed
  • A thread is not allowed to synchronize unassigned data from working memory back to main memory
  • A new variable must be created in main memory. Working memory is not allowed to use an uninitialized variable directly. Before performing use and store operations on variables, you must pass assign and load operations
  • Only one thread can lock a variable at a time. You must unlock the device for the same number of times
  • If a variable is locked, all of its values in working memory will be emptied. Before the execution engine can use the variable, the variable must be reloaded or assigned to initialize its value
  • You cannot unlock a variable if it is not locked. You cannot unlock a variable that is locked by another thread
  • Before an UNLOCK operation can be performed on a variable, it must be synchronized back to main memory

Some conventions for synchronization in the JMM:

  1. Shared variables must be flushed back to main memory immediately before the thread can be unlocked.
  2. Before a thread locks, it must read the latest value from main memory into working memory.
  3. Locking and unlocking are the same lock.

Visibility problem



Threads A and B running on the CPU copy the shared data number from main memory into their working memory, where thread A changes number to 2. But the change is not visible to thread B because the change has not yet been flushed into main memory.

To solve visibility problems, use thevolatileKeyword (requires modified variables to be updated to main memory immediately and read from main memory before each use) orlock(Read data from main memory before lock, update data to main memory before unlock).

Atomicity problem

Thread A and thread B share A number. Suppose thread A reads number from main memory into its working memory, and thread B also reads number into its working memory, and both threads increment number. At this point, the number plus one operation is executed twice, both in different working memory. If the two increments are performed sequentially, number adds 2 to the original value, and the number in main memory ends up being 3. Either thread A or thread B flushes the results into main memory first, and eventually the number in main memory increases only once to 2, even though there are two increments. To solve the atomicity problem, you can lock.

Order problem

Java orderliness is related to threads. If you look inside a thread, you’ll see that everything in the current thread is in order. If you look outside of a thread, you’ll see that all of its operations are out of order. There is a delay between the JMM’s working memory and main memory, and Java reorders some instructions. To solve ordering problems, volatile keywords or locking can be used to ensure that instructions are not reordered, that is, that the program is ordered.

Instruction reordering: reordering of compiler optimizations, possible instruction parallelism, and memory system reordering. After this rearrangement, the code will be executed!

There are two threads that need to perform some steps, a and B default to 0:

Thread A Thread B
a = 1;

x = b;
b = 2;

y = a;

Because of the indeterminacy of thread execution order, it is possible to have the following results:

  1. x = 0; y = 1;
  2. x = 2; y = 0;
  3. x = 2; y = 1;

Is it possible that x is equal to 0; y = 0; ? The answer is clearly yes! Because the instructions are rearranged, it is possible to execute the following situation:

Thread A Thread B
x = b;

a = 1;
y = a;

b = 2;

Instruction reordering No matter how an instruction is ordered, it must be guaranteed that the result of a single thread execution will not be changed (as-if-serial). The x variable in thread A depends on B, which was created before the program was run, as long as x = B; This step is ok after b is created. This is dependency).

volatile

Guaranteed visibility

Volatile requires that the modified variables be immediately updated to main memory and read from main memory before each use.

package com.example.volat;

import java.util.concurrent.TimeUnit;

public class VolatileTest {
  private static boolean flag = true;

  public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
      while (flag) {
      }
    }).start();
    TimeUnit.SECONDS.sleep(1);
    flag = false;
    System.out.println("flag: "+ flag); }}Copy the code

The normal logic of the above program should be that when flag is changed to false, the open thread exits the loop, and the program terminates. In fact, however, here’s what happens:

Because a thread has its own working memory, when it uses a variable, it copies the variable into its working memory, and another thread changes the value of the variable without knowing it! Simply add the volatile modifier to flag to ensure visibility.

  private volatile static boolean flag = true;
Copy the code

Atomicity is not guaranteed

package com.example.volat;

import java.util.concurrent.TimeUnit;

public class VolatileTest {
  private volatile static int number = 0;

  private static void increment(a) {
    number++;
  }

  public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 10; i++) {
      new Thread(() -> {
        for (int j = 0; j < 200; j++) increment();
      }).start();
    }
    TimeUnit.SECONDS.sleep(2); System.out.println(number); }}Copy the code

The result should be 2000, but it is not, and IDEA also gives a hint!



So here’s the question:number++;It’s just a line of code. How can it not be atomic?

becausenumber++;In fact:number = number + 1;The first step is to add one, and the second step is to assign the value to number. Decompile class files using Javapjavap -p -c VolatileTest.classThat is as follows:

How can atomicity be guaranteed without locking or synchronized?

Use atomic classes to ensure atomicity

AtomicInteger is the atomic class of Integer. To solve the atomicity problem, replace int with AtomicInteger as follows:

package com.example.volat;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class VolatileTest {
  private volatile static AtomicInteger number = new AtomicInteger();

  private static void increment(a) {
    number.getAndIncrement();
  }

  public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 10; i++) {
      new Thread(() -> {
        for (int j = 0; j < 200; j++) increment();
      }).start();
    }
    TimeUnit.SECONDS.sleep(2); System.out.println(number); }}Copy the code

So why is AtomicInteger guaranteed to be atomic?

// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();

public final int getAndIncrement(a) {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
Copy the code

To discover the broadening object method called, the code looks like this:

public final int getAndAddInt(Object var1, long var2, int var4) {
  int var5;
  do {// Spin locks are used here, explain spin locks later
    var5 = this.getIntVolatile(var1, var2);
  } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  return var5;
}

// Discover the local method called (compareAndSwap = CAS, more on optimistic locking below)
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
Copy the code

The Unsafe class is very special in that its methods are essentially native and tied to the operating system.

Ensure order

Volatile avoids instruction reordering.

Memory barriers prevent instructions from reordering, and the Java compiler inserts them at the appropriate places in the sequence of instructions to prevent reordering of a particular type of handler, thus allowing the program to follow its intended flow.

  1. Ensure the order in which certain operations are performed.
  2. Ensure that some data (or the result of an instruction) is visible in memory.

Volatile summary

Volatile guarantees visibility; No guarantee of atomicity; Because of memory barriers, instruction reordering can be avoided, thus ensuring orderliness. So where is volatile used the most? Singleton mode!

The singleton pattern

The hungry type

package com.example.single;

public class HungryMan {
  private static final HungryMan HUNGRY_MAN = new HungryMan();

  private HungryMan(a) {}public static HungryMan getInstance(a) {
    returnHUNGRY_MAN; }}Copy the code

Thread-safe, but the problem is that if the HungryMan contains a lot of variables and stores a lot of data, it can lead to a waste of resources.

LanHanShi

package com.example.single;

public class LazyMan {
  private static LazyMan lazyMan;

  private LazyMan(a) {}public static LazyMan getInstance(a) {
    if (lazyMan == null) {
      lazyMan = new LazyMan();
    }
    returnlazyMan; }}Copy the code

Thread is not safe!

DCL LanHanShi

package com.example.single;

public class DCLLazyMan {
  private volatile static DCLLazyMan lazyMan;

  private DCLLazyMan(a) {}public static DCLLazyMan getInstance(a) {
    if (lazyMan == null) {
      synchronized (DCLLazyMan.class) {
        if (lazyMan == null) {
          /* Object assignment is not atomic because it has three steps: * 1. Allocate memory * 2. Execute the constructor to initialize the object * 3. * Another thread calls this method after 1,3, and lazyMan has not been initialized yet. * So we must add the volatile modifier to lazyMan. * /
          lazyMan = newDCLLazyMan(); }}}returnlazyMan; }}Copy the code

Thread-safe, no resource hogging, and everything looks perfect!

Static inner class

package com.example.single;

public class StaticInnerClass {
  private StaticInnerClass(a) {}public static StaticInnerClass getInstance(a) {
    return InnerClass.INSTANCE;
  }

  private static class InnerClass {
    private static final StaticInnerClass INSTANCE = newStaticInnerClass(); }}Copy the code

When the external class is loaded, it does not need to load the inner class immediately. If the inner class is not loaded, it does not initialize INSTANCE, so it does not occupy memory. When the StaticInnerClass is loaded for the first time, there is no need to load the InnerClass. INSTANCE is initialized only when the getInstance() method is called for the first time. The first call to the getInstance() method causes the virtual machine to load the InnerClass class. This method not only ensures thread-safety, but also ensures singleton uniqueness, while delaying singleton instantiation.

Are the above four really safe?

Take the DCL slob for example:

  1. If the Serializable interface is implemented, new objects can be serialized and deserialized as follows:
package com.example.single;

import java.io.Serializable;

public class DCLLazyMan implements Serializable {
  public static final long serialVersionUID = 1L;

  private volatile static DCLLazyMan lazyMan;

  private DCLLazyMan(a) {}public static DCLLazyMan getInstance(a) {
    if (lazyMan == null) {
      synchronized (DCLLazyMan.class) {
        if (lazyMan == null) {
          lazyMan = newDCLLazyMan(); }}}returnlazyMan; }}Copy the code

DCLLazyMan. Java code

package com.example.single;

import java.io.*;

public class SecurityTest {
  public static void main(String[] args) throws IOException, ClassNotFoundException {
    DCLLazyMan instance = DCLLazyMan.getInstance();
    String filename = "d://dcl-lazyman";
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename));
    oos.writeObject(instance);
    oos.close();
    ObjectInputStream ois = new ObjectInputStream(newFileInputStream(filename)); Object o = ois.readObject(); System.out.println(o.getClass().getName()); System.out.println(o); System.out.println(instance); }}Copy the code

Securitytest.java tests the code

Test result: different memory address, indicating two objects! Decryption succeeded.

  1. Use reflection to break the singleton pattern with the following code:
package com.example.single;

public class DCLLazyMan {
  private volatile static DCLLazyMan lazyMan;
  // Add a flag to indicate whether it is instantiated
  private static boolean initialFlag = false;

  private DCLLazyMan(a) {
    synchronized (DCLLazyMan.class) {
      if (initialFlag) {
        throw new RuntimeException("Do not attempt to break singleton exceptions using reflection.");
      } else {
        initialFlag = true; }}}public static DCLLazyMan getInstance(a) {
    if (lazyMan == null) {
      synchronized (DCLLazyMan.class) {
        if (lazyMan == null) {
          lazyMan = newDCLLazyMan(); }}}returnlazyMan; }}Copy the code

DCLLazyMan. Java code

package com.example.single;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

public class SecurityTest {
  public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
    DCLLazyMan instance = DCLLazyMan.getInstance();
    InitialFlag = true; if initialFlag is not set to false, an error will be reported.
    Field initialFlag = DCLLazyMan.class.getDeclaredField("initialFlag");
    initialFlag.setAccessible(true);
    initialFlag.setBoolean(instance, false);
    Constructor<DCLLazyMan> constructor = DCLLazyMan.class.getDeclaredConstructor();
    constructor.setAccessible(true); DCLLazyMan newInstance = constructor.newInstance(); System.out.println(instance); System.out.println(newInstance); }}Copy the code

Securitytest.java tests the code

Test result: different memory address, indicating two objects! Decryption succeeded.

So how do you implement a secure singleton pattern? The enumeration.

The enumeration

package com.example.single;

public enum SingleEnum {
  INSTANCE;

  public SingleEnum getInstance(a) {
    returnINSTANCE; }}Copy the code

SingleEnum.java

  1. Using serialization cracking (Enumeration classes do not need to implement serialization interfaces)
package com.example.single;

import java.io.*;

public class EnumSecurityTest {
  public static void main(String[] args) throws IOException, ClassNotFoundException {
    SingleEnum instance = SingleEnum.getInstance();
    String filename = "d://enum";
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename));
    oos.writeObject(instance);
    oos.close();
    ObjectInputStream ois = new ObjectInputStream(newFileInputStream(filename)); Object o = ois.readObject(); System.out.println(o.getClass().getName()); System.out.println(o); System.out.println(instance); System.out.println(instance == o); }}Copy the code

Enumsecuritytest.java Test code

Test result: still true with ==! It’s the same object.

  1. Crack with reflection
package com.example.single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class EnumSecurityTest {
  public static void main(String[] args) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
    SingleEnum instance = SingleEnum.getInstance();
    Constructor<SingleEnum> constructor = SingleEnum.class.getDeclaredConstructor();
    constructor.setAccessible(true); SingleEnum newInstance = constructor.newInstance(); System.out.println(instance); System.out.println(newInstance); }}Copy the code

Enumsecuritytest.java Test code

Test results: unexpectedly reported no abnormal this method, what situation!

Analysis: the code is not deceptive, indicating that the enumeration class is compiled without a no-parameter constructor.

If idea is used to open the class file, we can find that there is no parameter constructor.

Javap -p singleenum.class Enumeration classes are also normal classes when compiled; 2. Even a parameter-free constructor. Javap may have fooled us.

Decomcompiled by Jad V1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name: SingleEnum.java

package com.example.single;


public final class SingleEnum extends Enum
{

    public static SingleEnum[] values()
    {
        return (SingleEnum[])$VALUES.clone();
    }

    public static SingleEnum valueOf(String name)
    {
        return (SingleEnum)Enum.valueOf(com/example/single/SingleEnum, name);
    }

    private SingleEnum(String s, int i)
    {
        super(s, i);
    }

    public static SingleEnum getInstance(a)
    {
        return INSTANCE;
    }

    public static final SingleEnum INSTANCE;
    private static final SingleEnum $VALUES[];

    static 
    {
        INSTANCE = new SingleEnum("INSTANCE".0);
        $VALUES = (newSingleEnum[] { INSTANCE }); }}Copy the code

Exe -p SingleEnum. Class > SingleEnum. Java to obtain the final decomcompiled source code as above, found two parameters of the private constructor. Jad decompile software download address

So go ahead and use reflection cracking!

package com.example.single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class EnumSecurityTest {
  public static void main(String[] args) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
    SingleEnum instance = SingleEnum.getInstance();
    Constructor<SingleEnum> constructor = SingleEnum.class.getDeclaredConstructor(String.class, int.class);
    constructor.setAccessible(true);
    SingleEnum newInstance = constructor.newInstance("INSTANCE".0); System.out.println(instance); System.out.println(newInstance); }}Copy the code

Modify the code ~

Test result: Finally found a restriction in reflection that prevents instantiation of enumerated classes. Therefore, enumeration ~ is recommended for singletons


CAS

Compare And Swap abbreviation, that is, Compare first, change if equal, otherwise not change.

package com.example.cas;

import java.util.concurrent.atomic.AtomicInteger;

public class CasTest {
  public static void main(String[] args) {
    AtomicInteger integer = new AtomicInteger(2020);
    System.out.println(integer.compareAndSet(2020.2021));
    System.out.println(integer.get());
    System.out.println(integer.compareAndSet(2020.2021)); System.out.println(integer.get()); System.out.println(integer.getAndIncrement()); }}Copy the code

GetAndIncrement () ¶

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;// is the memory offset address of the value property

static {
    try {
        valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw newError(ex); }}private volatile int value;

public final int getAndIncrement(a) {
    // The final call to the Unsafe class is the getAndAddInt method
    return unsafe.getAndAddInt(this, valueOffset, 1); } -- -- -- -- -- -- -- -- -- -- -- -- -- here are Unsafe class getAndAddInt method -- -- -- -- -- -- -- -- -- -- -- -- -- --public final int getAndAddInt(Object var1, long var2, int var4) {
  int var5;
  do {
    var5 = this.getIntVolatile(var1, var2);// Get the value in the memory address
  } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  // Memory operation, high efficiency
  / / the spin lock
  return var5;
}
Copy the code

CAS: compares the value in the current working memory to the value in the main memory, and if the value is expected, then the operation is performed; If not, keep repeating.

Disadvantages:

  1. The loop takes time;
  2. Only one shared variable can be guaranteed atomicity at a time;
  3. Can cause ABA problems.

ABA problem

Leopard cat for prince: Thread A will operate on the shared variable instance, and thread B will operate on the shared variable instance (let’s say instance.number = 2020). Thread A will first change it to 2021, and then to 2020. Thread B knows nothing about this, and then operates on the data. In fact, instance.number has been changed, and thread B should fail to perform the operation.

package com.example.aba;

import java.util.concurrent.atomic.AtomicInteger;

public class AbaTest {
  public static void main(String[] args) {
    AtomicInteger integer = new AtomicInteger(2020);
    // ============== disruptive thread ==================
    System.out.println(integer.compareAndSet(2020.2021));
    System.out.println(integer.get());
    System.out.println(integer.compareAndSet(2021.2020));
    System.out.println(integer.get());
    // ============== Desired thread ==================
    System.out.println(integer.compareAndSet(2020.6666)); System.out.println(integer.get()); }}Copy the code

How to solve THE ABA problem? Atomic operation with version number.

Atomic operation

package com.example.aba;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicReferenceTest {
  private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1.1);

  public static void main(String[] args) {
    // The offending thread modifies the target value and then restores it, but stamp keeps increasing
    new Thread(() -> {
      int stamp = atomicStampedReference.getStamp(); // Get the version number
      System.out.println("a-stamp-1: " + stamp);
      try {
        TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      atomicStampedReference.compareAndSet(1.2, stamp, stamp + 1);
      stamp = atomicStampedReference.getStamp();
      System.out.println("a-stamp-2: " + stamp);
      System.out.println(atomicStampedReference.compareAndSet(2.1, stamp, stamp + 1));
      System.out.println("a-stamp-3: " + atomicStampedReference.getStamp());
    }).start();
    // The same principle as optimistic locking
    // Obtain the version number first, then the business operation, and finally update the data
    new Thread(() -> {
      int stamp = atomicStampedReference.getStamp(); // Get the version number
      System.out.println("b-stamp-1: " + stamp);
      try {
        TimeUnit.SECONDS.sleep(2);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println(atomicStampedReference.compareAndSet(1.6, stamp, stamp + 1));
      System.out.println("b-stamp-2: "+ atomicStampedReference.getStamp()); }).start(); }}Copy the code

Note: the compareAndSet() method uses == at the bottom of the comparison, so it must be the same object or the execution will fail. The wrapper class caches some data. When valueOf() is used, the wrapper method checks whether it exists in the cache, returns the cached object if it does, and creates a new one if it does not.

-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- here are AtomicStampedReference source -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --public boolean compareAndSet(V expectedReference, V newReference,
                             int expectedStamp, int newStamp) {
    Pair<V> current = pair;
    returnexpectedReference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); } -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the following is a source of Integer -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

private static class IntegerCache {
    static final int low = -128;// Fix starting at -128
    static final int high;
    static final Integer cache[];// Cache the array

    static {
        // The value of high can be obtained through configuration
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if(integerCacheHighPropValue ! =null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);// Take the configured value and the larger of 127
                // The maximum length of the array is integer.max_value,
                // Ensure that I does not exceed integer. MAX_VALUE - (-low) -1,
                // Otherwise the array length will exceed integer.max_value
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If there is no configuration, or if it is not a number, ignore it
            }
        }
        high = h;
        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);
        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private IntegerCache(a) {}}Copy the code

The lock

Lock and unlock operations must occur in pairs, otherwise there will be problems.

Synchronized does not cause the above problems, but Lock does, as follows:

package com.example.lock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockTest {
  private Lock lock = new ReentrantLock();

  public static void main(String[] args) {
    LockTest lockTest = new LockTest();
// new Thread(lockTest::test1, "A").start();
// new Thread(lockTest::test1, "B").start();
    new Thread(lockTest::test2, "A").start();
    new Thread(lockTest::test2, "B").start();
  }

  // Add one more lock, deadlock will occur!
  public void test1(a) {
    lock.lock();
    lock.lock();
    try {
      System.out.println(Thread.currentThread().getName() + ": test1");
    } finally{ lock.unlock(); }}/ / a lock, will quote IllegalMonitorStateException
  public void test2(a) {
    lock.lock();
    try {
      System.out.println(Thread.currentThread().getName() + ": test2");
    } finally{ lock.unlock(); lock.unlock(); }}}Copy the code

Optimistic lock/pessimistic lock

Optimistic lock and pessimistic lock do not specifically refer to some two types of lock, is a concept or idea defined by people, mainly refers to the perspective of concurrent synchronization.

  1. Optimistic locking: Very optimistic, thinking that the data will not be modified by another thread when using the data, so do not add the lock, just to determine whether the data has been updated by another thread before. If the data is not updated, the current thread writes its modified data successfully. If the data has already been updated by another thread, different operations are performed (such as reporting an error or automatic retry) depending on the implementation.
    • In Java, this is done by using lock-free programming, most commonly the CAS algorithm, where incremental operations in Java atomic classes are implemented by CAS spin.
    • Optimistic lock is suitable for scenarios where many read operations are performed. The no-lock feature greatly improves the read operation performance.
  2. Pessimistic locking: the belief that data will be modified by another thread when it is used, so it will lock the data before it is acquired to ensure that the data will not be modified by another thread.
    • In Java, the implementation classes for synchronized and Lock are pessimistic locks.
    • Pessimistic locking applies to scenarios where many write operations are performed. The pessimistic locking ensures correct data during write operations.

Exclusive lock/shared lock

Exclusive and shared locks are also concepts.

  1. Exclusive lock: Also known as an exclusive lock, this lock can only be held by one thread at a time.
    • If thread T holds an exclusive lock on data A, no other thread can hold any type of lock on data A.
    • A thread that acquires an exclusive lock can both read and modify data.
    • In Java synchronized and already, ReentrantReadWriteLock. WriteLock is a mutex.
  2. Shared lock: The lock can be held by multiple threads.
    • If thread T adds A shared lock to data A, other threads can only add A shared lock to data A, not an exclusive lock.
    • The thread that acquires the shared lock can only read the data, not modify it.
    • In Java ReentrantReadWriteLock. ReadLock is Shared lock.

Exclusive lock and shared lock is to achieve through AQS, through the implementation of different methods, to achieve exclusive or shared.

Mutex/read-write lock

The exclusive/shared lock above is a broad term, and the mutex/read-write lock is an implementation.

  1. The implementation of mutexes in Java is ReentrantLock.
  2. Read and write lock in Java is ReentrantReadWriteLock, you can see JUC concurrent programming -ReadWriteLock, its performance is: read can coexist; Reading and writing cannot coexist; Writing can not coexist.

Reentrant lock/non-reentrant lock

  1. Reentrant lock: also known as recursive lock, this means that when the same thread obtains the lock from the outer method, it will automatically acquire the lock from the inner method (provided that the lock object is the same object or class). It will not block because the lock object has been acquired before and has not been released.
    • Synchronized and Lock implementation classes in Java are reentrant locks.
    • One advantage of reentrant locks is that deadlocks can be avoided to some extent.
  2. Non-reentrant lock: When the same thread obtains the lock from the outer method, it must release the lock from the outer method before entering the inner method again. If the lock is not released, a deadlock occurs.

Fair locks and unfair locks

  1. Fair lock: Very fair, locks need to queue, first come, first served.
  2. Unfair lock: Unfair lock can jump the queue.

Default to use unfair locks for fairness! Thread A takes 1 hour to execute the service, and thread B takes 1 second to execute the service. Therefore, it is not appropriate for thread B to wait for thread A to complete the service.

An unfair lock used by synchronized. ReentrantLock is a default unfair lock, but you can choose to use a fair lock. The code is as follows:

public ReentrantLock(a) {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
Copy the code

Bias lock/lightweight lock/heavyweight lock

These three types of locks refer to lock states and are specific to synchronized. In Java 5, efficient synchronized is implemented by introducing a mechanism of lock escalation. The status of these three locks is indicated by an object monitor field in the object header.

  1. Biased lock: a lock is automatically acquired by a thread that has been accessing a piece of synchronized code. Reduce the cost of acquiring locks.
  2. Lightweight lock: When a biased lock is accessed by another thread, the biased lock will be upgraded to a lightweight lock. Other threads will try to acquire the lock in the form of spin without blocking, improving performance.
  3. Heavyweight lock: when the lock is lightweight, the other thread spins, but the spin will not continue. When the spin is a certain number of times, the lock has not acquired the lock, it will enter the block, the lock expands to the heavyweight lock. A heavy lock would block the thread it applied for and degrade performance.

Segmented lock

Segment locking is a lock design, not a specific lock. For ConcurrentHashMap (JDK1.7, JDK1.8 does not use Segment), concurrency is implemented in the form of Segment locking to achieve efficient concurrent operations.

ConcurrentHashMap (JDK1.7, JDK1.8 does not use Segment) It is similar to the structure of HashMap (JDK7 and JDK8 implementation of HashMap), which has an array of entries, each element in the array is a linked list; It is also a ReentrantLock (the Segment inherits ReentrantLock).

  1. When a put element is required, instead of locking the entire HashMap, hashCode first knows which segment it wants to put in, and then locks that segment. So when a multithreaded put element is not placed in a segment, true parallel inserts are implemented.
  2. However, when we count size, which is to get the global information of the HashMap, we need to get all the segment locks before we count size.
  3. Segment locking is designed to refine the granularity of the lock. When an operation does not need to update the entire array, only one item in the array is locked.

spinlocks

In Java, a spin lock means that the thread trying to acquire the lock does not block immediately, but instead takescycleThe advantage is to reduce the cost of thread context switching, but the disadvantage is that loops consume CPU resources. Spin-locks are also implemented by CAS.

The picture above is a typical spin lock.

Hand-written non-reentrant spin locks

The non-reentrant spin lock code is as follows:

package com.example.lock;

import java.util.concurrent.atomic.AtomicReference;

public class MyLock {
  // The default value in atomicReference is null. Null indicates that no thread has acquired the lock. Non-null indicates that a thread has acquired the lock
  private AtomicReference<Thread> atomicReference = new AtomicReference<>();

  public void lock(a) {
    Thread currentThread = Thread.currentThread();
    System.out.println(currentThread.getName() + " want to get lock!");
    // If the value in the atomicReference is not null, then the loop continues, which is a non-reentrant lock
    while(! atomicReference.compareAndSet(null, currentThread)) {
      Thread.yield();// The current allocation of time slices can be discarded, reducing CPU consumption, but also introduces the consumption of thread context switch
    }
    System.out.println(currentThread.getName() + " get lock!");
  }

  public void unlock(a) {
    Thread currentThread = Thread.currentThread();
    boolean flag = atomicReference.compareAndSet(currentThread, null);
    if (flag) {
      System.out.println(currentThread.getName() + " release lock!");
    } else {
      throw new RuntimeException("Thread" + currentThread.getName() + "No lock obtained, cannot be released!"); }}}Copy the code

The test code is as follows:

package com.example.lock;

import java.util.concurrent.TimeUnit;

public class MyLockTest {
  private MyLock lock = new MyLock();

  public static void main(String[] args) throws InterruptedException {
    MyLockTest myLockTest = new MyLockTest();
    new Thread(myLockTest::sms, "A").start();
    TimeUnit.SECONDS.sleep(1);
    new Thread(myLockTest::call, "B").start();
// new Thread(myLockTest::both, "C").start();
  }

  public void sms(a) {
    lock.lock();
    try {
      TimeUnit.SECONDS.sleep(4);
      System.out.println(Thread.currentThread().getName() + ": sms");
    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally{ lock.unlock(); }}public void call(a) {
    lock.lock();
    try {
      System.out.println(Thread.currentThread().getName() + ": call");
    } finally{ lock.unlock(); }}public void both(a) {
    lock.lock();
    try {
      System.out.println(Thread.currentThread().getName() + ": sms");
      call();
    } finally{ lock.unlock(); }}}Copy the code

The test results are as follows:

  1. Multi-threaded lock acquisition, no problem

2. A single thread repeatedly acquires the lock, which is not reentrant and therefore deadlocks occur

Hand-written reentrant spin lock

The reentrant spin lock code is as follows:

package com.example.lock;

import java.util.concurrent.atomic.AtomicStampedReference;

public class MyReentrantLock {
  // Reference in atomicStampedReference indicates the thread holding the lock. The default value is null. Null indicates that no thread has obtained the lock. Non-null indicates that a thread has acquired the lock
  Stamp in atomicStampedReference indicates the lock depth. The default value is 0. 0 indicates that no thread has obtained the lock. Non-0 indicates that a thread has acquired the lock
  private AtomicStampedReference<Thread> atomicStampedReference = new AtomicStampedReference<>(null.0);

  public void lock(a) {
    Thread currentThread = Thread.currentThread();
    System.out.println(currentThread.getName() + " want to get lock!");
    // If the reference in atomicStampedReference is not null;
    // The reference in your atomicStampedReference is not currentThread;
    // So keep looping
    while(! atomicStampedReference.compareAndSet(null, currentThread, 0.1)
        && !atomicStampedReference.compareAndSet(currentThread, currentThread, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1)) {
      Thread.yield();// The current allocation of time slices can be discarded, reducing CPU consumption, but also introduces the consumption of thread context switch
    }
    System.out.println(currentThread.getName() + " get lock!");
  }

  public void unlock(a) {
    Thread currentThread = Thread.currentThread();
    int stamp;
    boolean flag = atomicStampedReference.getReference() == currentThread // Check whether it is the current thread
        && ((stamp = atomicStampedReference.getStamp()) > 1 ? // Perform the assignment and check if it is greater than 1
        // If greater than 1, the depth is reduced by 1
        atomicStampedReference.compareAndSet(currentThread, currentThread, stamp, stamp - 1)
        // Reset if equal to 1, otherwise return false
        : (stamp == 1 && atomicStampedReference.compareAndSet(currentThread, null.1.0)));
    if (flag) {
      System.out.println(currentThread.getName() + " release lock!");
    } else {
      throw new RuntimeException("Thread" + currentThread.getName() + "No lock obtained, cannot be released!"); }}}Copy the code

The test code is as follows:

package com.example.lock;

import java.util.concurrent.TimeUnit;

public class MyReentrantLockTest {
  private MyReentrantLock lock = new MyReentrantLock();

  public static void main(String[] args) throws InterruptedException {
    MyReentrantLockTest myLockTest = new MyReentrantLockTest();
    new Thread(myLockTest::sms, "A").start();
    TimeUnit.SECONDS.sleep(1);
    new Thread(myLockTest::call, "B").start();
// new Thread(myLockTest::both, "C").start();
// for (int i = 0; i < 10; i++) {
// new Thread(myLockTest::both, "D" + i).start();
/ /}
  }

  public void sms(a) {
    lock.lock();
    try {
      TimeUnit.SECONDS.sleep(4);
      System.out.println(Thread.currentThread().getName() + ": sms");
    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally{ lock.unlock(); }}public void call(a) {
    lock.lock();
    try {
      System.out.println(Thread.currentThread().getName() + ": call");
    } finally{ lock.unlock(); }}public void both(a) {
    lock.lock();
    try {
      System.out.println(Thread.currentThread().getName() + ": sms");
      call();
    } finally{ lock.unlock(); }}}Copy the code

The test results are as follows:

  1. Multi-threaded lock acquisition, no problem

2. A single thread repeatedly obtains the lock, no problem3. Multiple threads repeatedly acquire the lock, no problem

A deadlock

A deadlock is a situation in which multiple threads are blocked at the same time, and one or all of them are waiting for a resource to be released. Because the thread is blocked indefinitely, the program cannot terminate normally. There are four necessary conditions for Java deadlocks to occur:

  1. Exclusive use: When a resource is used by one thread, it cannot be used by another thread.
  2. Non-preemption: a resource requester cannot forcibly seize a resource from the possessor, and the resource can only be released voluntarily by the possessor.
  3. Request and hold: Resource requester maintains possession of the original resource while requesting other resources.
  4. Cyclic wait: P1 takes resources of P2, P2 takes resources of P3, and P3 takes resources of P1. It creates a waiting loop.

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

Hand write a deadlock

package com.example.die;

import java.util.concurrent.TimeUnit;

@SuppressWarnings("ALL")
public class DieLockTest {
  public static void main(String[] args) {
    String A = "A", B = "B", C = "C";
    new Thread(() -> {// Hold A, want B
      synchronized (A) {
        System.out.println(Thread.currentThread().getName() + " has " + A);
        try {
          TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " wants " + B);
        synchronized (B) {
          System.out.println(Thread.currentThread().getName() + " has "+ B); }}},"T1").start();
    new Thread(() -> {// Hold B, want C
      synchronized (B) {
        System.out.println(Thread.currentThread().getName() + " has " + B);
        try {
          TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " wants " + C);
        synchronized (C) {
          System.out.println(Thread.currentThread().getName() + " has "+ C); }}},"T2").start();
    new Thread(() -> {// Hold C, want A
      synchronized (C) {
        System.out.println(Thread.currentThread().getName() + " has " + C);
        try {
          TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " wants " + A);
        synchronized (A) {
          System.out.println(Thread.currentThread().getName() + " has "+ A); }}},"T3").start(); }}Copy the code

Deadlock found after running program:

Screen deadlock

  1. JPS + jStack: all command tools provided by the JDK

JPS -l: Displays the process ID

Jstack Process ID: View stack information

2. Jconsole: a visual tool provided by the JDK in the JDK /bin directory

Open the jConsole connection to the specified process

Select an unsafe connection because a secure connection failure may occur

Select the thread and click Detect deadlock

Click on any thread and you can see that the resource is occupied by another thread

3. Jvisualvm: JDK provides a very powerful troubleshooting Java program problems tool, can monitor the performance of the program, view JVM configuration information, heap snapshot, thread stack information, in the JDK /bin directory

Double-click the selection process, click on the thread, you can find a direct message “deadlock detected!”

Click “thread Dump” and drag to the end to see the same content generated by JStack

Break the deadlock

To break a deadlock, you only need to break one of the four conditions.

  1. Break mutual exclusion use: Mutual exclusion is necessary for a non-shared resource and cannot be changed, but should be guaranteed.
  2. Destruction is not preemptible:
    • When a thread that already holds some resources makes new resource requests and is not satisfied, it must release all resources it has held and reapply for them when it needs to use them later.
      • This method is more complex to implement, and the cost is relatively high. Releasing the resources that have been held may lead to the failure of the previous work of the thread, etc. Repeated application and release of resources will lead to infinite delay of the execution of the thread, which will not only prolong the turnover cycle of the thread, but also affect the throughput of the system.
  3. Break request and hold: There are two ways.
    • The resource requester starts by requesting all the resources it needs for the entire run at once.
      • Advantages: Simple, easy to implement and safe.
      • Disadvantages: A resource cannot be started because it is not satisfied, and other resources that have been satisfied cannot be used, which seriously reduces resource utilization and causes resource waste.
    • The resource requester applies for some resources in stages. At the beginning of each phase, the requester applies for some resources and releases these resources at the end of each phase.
      • Is an improvement on the above method, the utilization of resources will be improved.
  4. Break loop wait: just lock in order and unlock in reverse order.
    • For example, the above test code requires A -> B -> C order to lock, then destroy the loop wait, only need to change the thread T3, the code is as follows:
    new Thread(() -> {// Hold A, then C, to solve the deadlock problem
      synchronized (A) {
        System.out.println(Thread.currentThread().getName() + " has " + A);
        try {
          TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " wants " + C);
        synchronized (C) {
          System.out.println(Thread.currentThread().getName() + " has "+ C); }}},"T3").start();
Copy the code