Beauty of concurrent programming (basic) – Notes

Time:2020-11-19

Reading the beauty of Java Concurrent Programming

The first chapter is the thread foundation of concurrent programming

What is thread

Relationship between process and thread:

  • Thread is an entity in the process, and the thread itself will not exist independently
  • Process is the basic unit of system resource allocation and scheduling
  • There is at least one thread in a process, and multiple threads in the process share the resources of the process
  • CPU resources are special and are allocated to threads

JAVA memory area: multiple threads in a process share the process heap and method area, but the thread has its own program counter, virtual machine stack and local method stack

img

Source:JAVA memory area (runtime data area) details, the difference between JDK1.8 and jdk1.7 – Jie 0327

Creation and running of thread

There are three ways to create threads:

  1. Inherit thread class
  2. Implement runnable interface
  3. Using futuretask class

Directly inherit thread and override run() method:

public class MyThread extends Thread{
    @Override
    public void run() {
        // code...
    }
}
new MyThread().start();

Implement runnable interface:

public class Task implements Runnable{
    @Override
    public void run() {
        // code...
    }
}
new Thread(new Task()).start();

The callable interface can be implemented with return values:

public class Task implements Callable {

    @Override
    public Integer call() throws Exception {
        return null;
    }
}
FutureTask futureTask = new FutureTask<>(new Task());
new Thread(futureTask).start();
futureTask.get();

Conclusion: the method of inheriting thread does not separate the task logic from the thread mechanism, and a thread needs to be created every time a task is executed. Using runnable or callable interfaces, you can use a single thread to perform multiple tasks. (the most direct way is to submit the task to the thread pool)

Waiting and notification of threads

//Methods to block threads until:
//* other threads called notify() or notifyAll ()
//* if the thread is interrupted, an interrupt exception will be thrown
//* thread timeout return
//* false Awakening
obj.wait();
obj.wait(5000);

obj.notify();
obj.notifyAll();

Call the above methods to obtain the monitor lock of the object. There are two ways to obtain the monitor lock of the object:

//Gets the monitor lock for the object itself
public synchronized void method(){
    while (!condition)
    	wait();
}
//Gets the monitor lock of obj object
synchronized(obj){
    while (!condition)
        obj.wait();
}
  • After getting the monitor lock, check whether the conditions are met, or call wait() to suspend the thread and release the lock (the while loop is used to avoid false wake-up)
  • When other threads wake up the waiting thread, the awakened thread will first compete for the lock, and the thread that gets the lock will return from the wait() method and check the condition again

Methods in threads

thread.join (); // suspend the calling thread until the execution of the target thread ends
Thread.sleep (1000); // does not participate in CPU scheduling within the specified time (static method)
Thread.yield (); // prompt CPU thread to discard CPU time in advance (static method)

Thread interrupt:

thread.interrupt (); // interrupt thread object 
thread.isInterrupted (); // judge that the thread object is an interrupt
Thread.interrupted (); // determine whether the calling thread is interrupted and clear the interrupt flag (static method)

The interrupt thread simply places the flag, and how it responds depends on the thread itself (it may throw an exception or continue execution)

ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock (); // wait for the thread to obtain the lock and then throw an interrupt exception
reentrantLock.lockInterruptibly (); // throw thread interrupt immediately

Thread context switch

There are two scheduling modes: preemptive and non preemptive

In the preemptive system, the thread takes up CPU by polling time slice. When the time slice is used up, the CPU is released to perform thread context switch.

This scheduling method determines that when executing CPU intensive tasks, the maximum number of threads that can be started is the same as the number of CPU; while when executing IO intensive tasks, more threads can be started.

thread deadlock

Deadlock refers to the phenomenon of two or more threads waiting for each other due to competing for resources in the process of execution. Without external force, these threads will wait all the time and cannot continue to run.

There are four prerequisites for Deadlock:

  • Mutex condition: resources can only be held by one thread (exclusive)
  • Request and hold: the thread already holds the resource and requests other resources held by other threads
  • Unalienable condition: the held resource can only be released by the thread that holds it
  • Loop waiting condition: requests for resources form a ring chain

Methods to solve deadlock:

  • Orderly principle of resource application
  • (InnoDB) request timeout, graph algorithm

Guardian thread and user thread

The JVM process exits after all user threads have ended, regardless of whether there is a daemon thread executing or not.

Thread thread = new Thread();
thread.setDaemon(true);
thread.start();

ThreadLocal

Each thread has its own simpledateformat object:

//As you can see from the comments on simpledateformat, simpledateformat is not thread safe
private ThreadLocal DateFormatContext = ThreadLocal.withInitial(()->{
    return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
});

Principle of ThreadLocal implementation:

img

Source:ThreadLocal explained – DuyHai

Thread.set () method:

//Get the threadlocals variable of the current thread and put the value pair in it
//And threadlocals is lazy loaded
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

Thread local variables: theadlocalmap is a thread local hash table

public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

Threadlocals for lazy loading:

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
 * The initial capacity -- MUST be a power of two.
 */
private static final int INITIAL_CAPACITY = 16;

ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

ThreadLocal.get () method:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //Take theadlocal object as the key, and take the value from the local hash table of the thread
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

If the map or the corresponding key has not been initialized, the initial value is returned:

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

The initial value is null by default: the example above sets the initial value to the simpledateformat object

protected T initialValue() {
    return null;
}
public static  ThreadLocal withInitial(Supplier extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

static final class SuppliedThreadLocal extends ThreadLocal {

    private final Supplier extends T> supplier;

    SuppliedThreadLocal(Supplier extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }
	
    //So the essence of the supplier is a factory, and the threadlocalmap is a thread local container
    // supplier.get () returns the newly created object
    @Override
    protected T initialValue() {
        return supplier.get();
    }
}

ThreadLocal.remove (): remove local objects when they are not in use to prevent memory overflow

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

ThreadLocal concludes:

  • ThreadLocal is actually a tool shell. It will operate the thread local hash table threadlocals, and the hash table takes the ThreadLocal object as the key
  • Thread locales, which are local to the thread, are lazily loaded with an initial capacity of 16
  • Remove() should be called after the object is not used to avoid memory overflow

InheritableThreadLocal

With theadlocal, the child thread cannot access the local variables of the parent thread, which is solved by inheritablethreadlocal.

Source code analysis:

public class InheritableThreadLocal extends ThreadLocal {
	//Calculate the value of the local variable of the child thread according to the value of the local variable of the parent thread (the original value is returned directly here)
    protected T childValue(T parentValue) {
        return parentValue;
    }

    //Override the method of the parent class ThreadLocal, replacing threadlocals with inheritablethreadlocals
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

When the thread is created, the init () method is called: the local variables of the parent thread are shallow copied to the child thread

public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc) {

    //...
    
    //If the parent thread's inheritablethreadlocals is not null, execute the following code
    if (parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
	
    //...
}
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

    //Variable parent thread hash key value pair
    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            //If the key is not null
            @SuppressWarnings("unchecked")
            ThreadLocal key = (ThreadLocal) e.get();
            if (key != null) {
                //Gets the value corresponding to the key
                Object value = key.childValue(e.value);
                //Create a new key value pair
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                //Using linear probe to solve hash conflict
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

Heritabilethreadlocal summary: when creating a thread, inheritablethreadlocal copies the local variables of the parent thread to the child thread, so that the child thread can access the local variables of the parent thread.

Chapter 2 other basic knowledge of concurrent programming

What is multithreading concurrent programming

The concept of concurrency and parallelism:

  • Concurrency refers to the simultaneous execution of multiple tasks in the same time period
  • Parallel refers to the execution of multiple tasks in unit time

A time period consists of multiple units of time

Task type:

  • IO intensive tasks: for IO intensive tasks, we should try our best to reduce the CPU usage and CPU idle time when thread blocking
  • CPU intensive tasks: for CPU intensive tasks, we should minimize the overhead of thread context switching

Thread safety in Java

Shared resources: resources that can be read and written by multiple threads

When multiple threads read and write to shared resources at the same time, they must be synchronized.

According to the JAVA memory model, all variables are stored in the main memory. When a thread uses variables, it will copy the variables in memory to its working memory. When a thread reads and writes variables, it operates on the variables in its working memory.

In this case, it is necessary to ensure the atomicity of shared resource operation through synchronization mechanism

image-20201102152438158

Memory visibility of shared variables in Java

The memory model of Java is an abstract concept. The working memory corresponds to the hardware architecture, which is the memory in CPU, the first level cache and the second level cache.

image-20201102152855951

Memory visibility issues caused by caching:

  1. Thread a reads and writes the shared variable 1 and synchronizes the result to the cache and main memory
  2. Thread B reads and writes the shared variable 1 (from the L2 cache) and synchronizes the results to the cache and memory
  3. At this point, thread a will read and write the shared variable a here, and dirty data will be read from the first level cache

Synchronized keyword in Java

synchronizedIs a monitor lock inside the object. It is an exclusive lock.

Memory semantics of synchronized:

  • The memory semantics of entering the synchronized block is to clear the variables used in the synchronized block from the working memory of the thread, so that when the variable is used in the synchronized block, it will not be obtained from the working memory of the thread, but directly from the main memory;
  • The semantics of exiting synchronized is to refresh the modification of shared variables in the synchronized block to main memory.

Synchronized can solve the problem of visibility of shared variable memory, and can also be used to implement atomic operations

Volatile keyword in Java

When a variable is declared volatile, the thread will not cache the value in the register or other places when writing the variable, but will refresh the value to the main memory; when other threads read the shared variable, it will get the latest value from the main memory rewriting instead of using the value in the working memory of the current thread.

Volatile can solve the problem of visibility of shared variable memory and reordering of instructions, but it can not guarantee the atomicity of operations

CAS operation in Java

CAS ensures atomicity of operation through hardware.

If the value to be manipulated has a circular conversion, there may be ABA problems using CAS algorithm. The solution is to add an incremental version number or timestamp.

Unsafe class

Methods in the unsafe class:

//Gets the offset value of the variable
public native long objectFieldOffset(Field f);
//Gets the address of the first element of the array
public native int arrayBaseOffset(Class> arrayClass);
//Gets the bytes occupied by an element of an array
public native int arrayIndexScale(Class> arrayClass);

//Update atomically
public final native boolean compareAndSwapLong(Object o, long offset,
                                               long expected,
                                               long x);

//Gets a value of type long (with volatile semantics)
public native long getLongVolatile(Object o, long offset);
//Set a value of type long (with volatile semantics)
public native void putLongVolatile(Object o, long offset, long x);
//Set a value of type long (without volatile semantics)
public native void putOrderedLong(Object o, long offset, long x);


//Block current thread
public native void park(boolean isAbsolute, long time);
//Wakes the specified thread
public native void unpark(Object thread);
//Encapsulation of CAS algorithm
public final long getAndSetLong(Object o, long offset, long newValue) {
    long v;
    do {
        v = getLongVolatile(o, offset);
    } while (!compareAndSwapLong(o, offset, v, newValue));
    return v;
}

public final long getAndAddLong(Object o, long offset, long delta) {
    long v;
    do {
        v = getLongVolatile(o, offset);
    } while (!compareAndSwapLong(o, offset, v, v + delta));
    return v;
}

Get unsafe object:

public final class Unsafe {

    private static native void registerNatives();
    static {
        registerNatives();
        sun.reflect.Reflection.registerMethodsToFilter(Unsafe.class, "getUnsafe");
    }

    private Unsafe() {}

    private static final Unsafe theUnsafe = new Unsafe();
    
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class> caller = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(caller.getClassLoader()))
            throw new SecurityException("Unsafe");
        return theUnsafe;
    }
}

Due to the limitation of unsafe class, reflection is needed to obtain unsafe objects

public class UnsafeTest {
    volatile long value;
    static long valueOffset;
    static Unsafe unsafe;

    static {
        try {
            Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
            unsafeField.setAccessible(true);
            unsafe = (Unsafe) unsafeField.get(null);
            valueOffset = unsafe.objectFieldOffset(UnsafeTest.class.getDeclaredField("value"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        UnsafeTest test = new UnsafeTest();
        boolean success = UnsafeTest.unsafe.compareAndSwapLong(test, UnsafeTest.valueOffset, 0, 1);
        System.out.println(success);
        System.out.println(test.value);
    }
}

Instruction reordering

The JAVA memory model allows compilers and processors to reorder instructions to improve performance.

Reordering in single thread can ensure that the final execution result is consistent with that of program sequence execution, but there will be problems in multithreading.

usevolatileIt can solve the problem caused by instruction reordering.

Pseudo sharing

The reason for pseudo sharing is that the unit of data exchange between cache and main memory is cache row. When multithreading modifies different variables of the same cache line, only one thread can modify the variable of cache line, because cache consistency protocol will make the same cache line of other threads invalid, so that the thread can only read from secondary cache or main memory again, resulting in performance Down.

In single thread, cache line can make full use of the locality principle of program running, so as to improve program performance.

The method of multithreading to solve the cache line:

  1. Byte padding
  2. @sun.misc.Contendedannotation

use@ContendedThe annotation requires the use of parameters:-XX:-RestrictContended

The default width is 128 bytes and can be used-XX:ContendedPaddingWidthCustom width;

Overview of locks

  • Pessimistic lock and optimistic lock
  • Exclusive lock and shared lock
  • Fair lock and unfair lock
  • Reentrant lock
  • Spinlock (the default number is 10,-XX:PreBlockSpinsh