juc concurrent programming learning notes (Shang Si Valley)

Keywords: Java JUC lock synchronized

1. What is JUC

1.1 introduction to JUC

JUC is short for Java. Util. Concurrent toolkit. This is a toolkit for processing threads. JDK 1.5 began to appear.

1.2 processes and threads

Process is a running activity of a computer program on a data set. It is the basic unit for resource allocation and scheduling of the system and the basis of the structure of the operating system. In the contemporary computer architecture of thread oriented design, process is the container of thread. Program is the description of instructions, data and their organizational form, and process is the entity of program. It is a running activity of a program in a computer on a data set. It is the basic unit for resource allocation and scheduling of the system and the basis of the structure of the operating system. Program is the description of instructions, data and their organizational form, and process is the entity of program.

Thread is the smallest unit that the operating system can schedule operations. It is included in the process and is the actual operation unit in the process. A thread refers to a single sequential control flow in a process. Multiple threads can be concurrent in a process, and each thread executes different tasks in parallel.

1.3 thread status

1.3.1 thread state enumeration class

public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }

1.3.2 difference between wait / sleep

(1) sleep is the static method of Thread, and wait is the method of Object, which can be called by any Object instance.
(2) sleep does not release the lock, nor does it need to occupy the lock. wait releases the lock, but it is called on the premise that the current thread occupies the lock (that is, the code should be in synchronized).
(3) They can all be interrupted by the interrupted method.

1.4 concurrency and parallelism

Concurrency: multiple threads are accessing the same resource at the same time, and multiple threads are connected to one point
Example: Spring Festival transportation ticket grabbing e-commerce spike
Parallel: multiple tasks are executed together and then summarized
Example: make instant noodles, boil water in an electric kettle, tear the seasoning and pour it into the bucket

1.5 tube side

Monitor ensures that only one process is active in the process at the same time, that is, the operations defined in the process are called by only one process at the same time (implemented by the compiler). However, this does not ensure that the processes execute in the designed order
Synchronization in the JVM is implemented based on the entry and exit monitor objects. Each object will have a monitor object, which will be created and destroyed together with the java object
The execution thread must first hold the pipe process object before executing the method. When the method is completed, the pipe process will be released. The method will hold the pipe process during execution, and other threads can no longer obtain the same pipe process

1.6 user threads and daemon threads

User thread: ordinary thread and user-defined thread used at ordinary times
Daemon thread: running in the background, it is a special thread, such as garbage collection
When the main thread ends, the user thread is still running and the JVM survives
If there are no user threads, they are all daemon threads, and the JVM ends

2. lock interface

2.1 synchronized keyword review

synchronized is a keyword in Java. It is a kind of synchronous lock. It modifies the following objects:

1. Modify a code block. The modified code block is called synchronization statement block. Its scope of action is the code enclosed in braces {}, and the object of action is the object calling the code block;
2. Modify a method. The modified method is called synchronous method. Its scope of action is the whole method, and the object of action is the object calling the method;
3. Modify a static method. Its scope of action is the whole static method, and the object of action is all objects of this class;
4. Modify a class. Its scope of action is the part enclosed in parentheses after synchronized. The main object of action is all objects of this class.

If a code block is modified by synchronized, when a thread acquires the corresponding lock and executes the code block, other threads can only wait all the time. The thread waiting to acquire the lock releases the lock. Here, the thread acquiring the lock releases the lock in only two cases:
1) The thread that obtains the lock executes the code block, and then the thread releases the possession of the lock;
2) When an exception occurs in thread execution, the JVM will let the thread automatically release the lock.
Then, if the thread that obtains the lock is blocked due to waiting for IO or other reasons (such as calling the sleep method), but does not release the lock, other threads can only wait dryly. Imagine how this affects the execution efficiency of the program.
Therefore, a mechanism is needed to prevent the waiting thread from waiting indefinitely (for example, only waiting for a certain time or being able to respond to interrupts), which can be done through Lock.

2.2 what is lock

The difference between Lock and Synchronized
• lock is not built-in in the Java language. synchronized is the keyword of the Java language, so it is a built-in feature. Lock is a class through which synchronous access can be realized;
• there is a big difference between lock and synchronized. Using synchronized does not require the user to manually release the lock. When the synchronized method or synchronized code block is executed, the system will automatically let the thread release the occupation of the lock; Lock requires the user to release the lock manually. If the lock is not released actively, it may lead to deadlock.

2.2.1 lock interface

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

2.2.2lock()

The lock() method is one of the most commonly used methods, which is used to obtain locks. Wait if the lock has been acquired by another thread.
With Lock, the Lock must be released actively, and the Lock will not be released automatically in case of abnormality. Therefore, generally speaking, the use of Lock must be carried out in the try{}catch {} block, and the operation of releasing the Lock must be carried out in the finally block to ensure that the Lock must be released and prevent deadlock. Generally, Lock is used for synchronization in the following form:

Lock lock = ...; 
lock.lock(); 
try{ 
	//Processing tasks 
} catch(Exception ex){ 

} finally{
	lock.unlock(); //Release lock 
}

2.2.3 newCondition

The keyword synchronized is used together with wait()/notify() to implement the wait / notify mode. The newcondition() method of Lock returns the Condition object. The Condition class can also implement the wait / notify mode.
When notifying with notify(), the JVM will wake up a waiting thread randomly. Selective notification can be made by using Condition class. There are two commonly used methods of Condition:
• await() will make the current thread wait and release the lock. When other threads call signal(), the thread will regain the lock and continue to execute.
• signal() is used to wake up a waiting thread.

Note: before calling the await()/signal() method of Condition, the thread also needs to hold the relevant Lock. After calling await(), the thread will release the Lock. After calling singal(), it will wake up a thread from the waiting queue of the current Condition object. The awakened thread attempts to obtain the Lock. Once the Lock is obtained, it will continue to execute.

2.3 ReentrantLock

ReentrantLock means "reentrant lock". The concept of reentrant lock will be described later.
ReentrantLock is the only class that implements the Lock interface, and ReentrantLock provides more methods.

2.4 ReadWriteLock

ReadWriteLock is also an interface, in which only two methods are defined:

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

One is used to obtain a read lock and the other is used to obtain a write lock. In other words, the read and write operations of files are separated and divided into two locks to be allocated to threads, so that multiple threads can read at the same time. The following ReentrantReadWriteLock implements the ReadWriteLock interface.
ReentrantReadWriteLock provides many rich methods, but there are two main methods: readLock() and writeLock() to obtain read and write locks.

be careful:

  • If a thread has occupied a read lock, if other threads want to apply for a write lock, the thread applying for a write lock will wait to release the read lock.
  • If a thread has occupied a write lock, if other threads apply for a write lock or a read lock, the requesting thread will wait for the write lock to be released.

2.5 difference between lock and synchronized

  1. Lock is an interface, synchronized is a keyword in Java, and synchronized is a built-in language implementation;
  2. synchronized will automatically release the Lock held by the thread when an exception occurs, so it will not cause deadlock; When an exception occurs in Lock, if you do not actively release the Lock through unLock(), it is likely to cause deadlock. Therefore, when using Lock, you need to release the Lock in the finally block;
  3. Lock allows the thread waiting for the lock to respond to the interrupt, but synchronized does not. When synchronized is used, the waiting thread will wait all the time and cannot respond to the interrupt;
  4. Through Lock, you can know whether you have successfully obtained the Lock, but synchronized cannot.
  5. Lock can improve the efficiency of reading operations by multiple threads.
    In terms of performance, if the competition for resources is not fierce, the performance of the two is similar. When the competition for resources is very fierce (that is, a large number of threads compete at the same time), the performance of Lock is much better than that of synchronized.

2.6 synchronized implementation case

public class Ticket {
    private Integer count = 30;

    public synchronized void sale(){
        if (count > 0){
            System.out.println(Thread.currentThread().getName() + "Sell No" + (count--) + "Ticket");
        }
    }
}

Test:

	public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                ticket.sale();
            }
        },"AA").start();

        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                ticket.sale();
            }
        },"BB").start();

        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                ticket.sale();
            }
        },"CC").start();
    }

2.7 lock implementation case

public class LTicket {
    private Integer count = 30;

    private ReentrantLock lock = new ReentrantLock();

    public void sale(){
        lock.lock();
        try {
            if (count > 0){
                System.out.println(Thread.currentThread().getName() + "Sell No" + (count--) + "Ticket");
            }
        } catch (Exception e){

        } finally {
            lock.unlock();
        }
    }
}

Test:

	public static void main(String[] args) {
        LTicket ticket = new LTicket();
        new Thread(()->{
            for (int i = 0; i < 30; i++) {
                ticket.sale();
            }
        }, "AA").start();

        new Thread(()->{
            for (int i = 0; i < 30; i++) {
                ticket.sale();
            }
        }, "BB").start();

        new Thread(()->{
            for (int i = 0; i < 30; i++) {
                ticket.sale();
            }
        }, "CC").start();
    }

3. Inter thread communication

There are two models of inter thread communication: shared memory and message passing. The following methods are implemented based on these two models. Let's analyze a common interview question:

Scenario - two threads. One thread adds 1 to the current value, and the other thread subtracts 1 to the current value. Inter thread communication is required

3.1 synchronized scheme

Resource class:

public class DemoEntity {
    private int number = 0;

    public synchronized void incr() {
        try {
            while (number > 0){
                this.wait();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "::" + number);
            notifyAll();
        } catch (Exception e){
            e.printStackTrace();
        }
    }

    public synchronized void decr() {
        try {
            while (number <= 0){
                this.wait();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "::" + number);
            notifyAll();
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

Multithreaded call:

public static void main(String[] args) {
        DemoEntity demoEntity = new DemoEntity();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                demoEntity.incr();
            }
        }, "AA").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                demoEntity.decr();
            }
        }, "BB").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                demoEntity.incr();
            }
        }, "CC").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                demoEntity.decr();
            }
        }, "DD").start();
    }

3.2 lock scheme

Resource class:

public class LDemoEntity {
    private int number = 0;

    private Lock lock = new ReentrantLock();

    private Condition condition = lock.newCondition();

    public void incr() {
        lock.lock();
        try {
            while (number > 0){
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "::" + number);
            condition.signalAll();
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void decr() {
        lock.lock();
        try {
            while (number <= 0){
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "::" + number);
            condition.signalAll();
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

Multithreaded call:

public static void main(String[] args) {
    LDemoEntity demoEntity = new LDemoEntity();
    new Thread(()->{
        for (int i = 0; i < 10; i++) {
            demoEntity.incr();
        }
    }, "AA").start();

    new Thread(()->{
        for (int i = 0; i < 10; i++) {
            demoEntity.decr();
        }
    }, "BB").start();

    new Thread(()->{
        for (int i = 0; i < 10; i++) {
            demoEntity.incr();
        }
    }, "CC").start();

    new Thread(()->{
        for (int i = 0; i < 10; i++) {
            demoEntity.decr();
        }
    }, "DD").start();
}

3.3 customized communication between threads

Resource class:

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

/**
 * @author lyz
 * @Title: CustomDemoEntity
 * @Description:
 * @date 2021/9/13 13:14
 */
public class CustomDemoEntity {
    private int flag = 1;

    private Lock lock = new ReentrantLock();

    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    public void printA(int loop){
        lock.lock();
        try {
            while (flag != 1){
                c1.await();
            }
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "::The first" + loop + "Secondary cycle::" + flag);
            }
            flag = 2;
            c2.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB(int loop){
        lock.lock();
        try {
            while (flag != 2){
                c2.await();
            }
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "::The first" + loop + "Secondary cycle::" + flag);
            }
            flag = 3;
            c3.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printC(int loop){
        lock.lock();
        try {
            while (flag != 3){
                c3.await();
            }
            for (int i = 0; i < 15; i++) {
                System.out.println(Thread.currentThread().getName() + "::The first" + loop + "Secondary cycle::" + flag);
            }
            flag = 1;
            c1.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

Multithreaded call:

public static void main(String[] args) {
        CustomDemoEntity demoEntity = new CustomDemoEntity();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                demoEntity.printA(i);
            }
        }, "AA").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                demoEntity.printB(i);
            }
        }, "BB").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                demoEntity.printC(i);
            }
        }, "CC").start();
    }

3.4 multithreaded programming steps

1. Create a resource class, and create attributes and operation methods in the resource class

  • judge
  • work
  • notice

2. Create multiple threads and call the operation methods of the resource class

3. Prevent false awakening

4. Thread safety for collections

4.1ArrayList thread safety issues

Test code:

public static void main(String[] args) {
//        List<String> list = new ArrayList<>();
//        List<String> list = new Vector<>();
//        List<String> list = Collections.synchronizedList(new ArrayList<>());
        List<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString());
                System.out.println(list);
            }, "aa").start();
        }
    }

java.util.ConcurrentModificationException occurs when the add method of ArrayList reads and writes concurrently. It is not a thread safe collection.

Use Vector or collections.synchronized list to ensure thread safety during list concurrent execution (synchronized is used at the bottom).

4.2CopyOnWriteArrayList

It is equivalent to a thread safe ArrayList. Like ArrayList, it is a variable array; However, unlike ArrayList, it has the following features:

  1. It is most suitable for applications with the following characteristics: the List size is usually kept small, read-only operations are far more than variable operations, and inter thread conflicts need to be prevented during traversal.
  2. It is thread safe.
  3. Because you usually need to copy the entire underlying array, variable operations (add(), set(), remove(), and so on) are expensive.
  4. Iterators support immutable operations such as hasNext(), next(), but do not support immutable operations such as remove().
  5. Traversal using iterators is fast and does not conflict with other threads. When constructing iterators, iterators rely on invariant array snapshots.

When we add elements to a container, instead of directly adding them to the current container, we first Copy the current container, Copy a new container, and then add elements to the new container. After adding elements, we point the reference of the original container to the new container.

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

    /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;

    final Object[] getArray() {
        return array;
    }
    final void setArray(Object[] a) {
        array = a;
    }
    
    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     */
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

4.3 thread safety of HashSet

Test code:

public static void main(String[] args) {
//        Set<String> list = new HashSet<>();
//        Set<String> list = Collections.synchronizedSet(new HashSet<>());
        Set<String> list = new CopyOnWriteArraySet<>();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0,6));
                System.out.println(list);
            }, "aa").start();
        }
    }

Use Collections.synchronizedSet or CopyOnWriteArraySet to resolve thread safety issues.

4.4 HashMap thread safety issues

Test code:

public static void main(String[] args) {
//        Map<String, String> list = new HashMap<>();
//        Map<String, String> list = Collections.synchronizedMap(new HashMap<>());
        Map<String, String> list = new ConcurrentHashMap<>();
        for (int i = 0; i < 20; i++) {
            String key = String.valueOf(i);
            new Thread(() -> {
                list.put(key, UUID.randomUUID().toString().substring(0,6));
                System.out.println(list);
            }, "aa").start();
        }
    }

Use Collections.synchronizedMap or ConcurrentHashMap to solve the thread safety problem of Map collections.

4.5 summary

1. Thread safe and thread unsafe collection

There are two types of collection types: thread safe and thread unsafe, such as:

ArrayList ----- Vector

HashMap -----HashTable

However, the above are implemented through the synchronized keyword, which is inefficient

2. Thread safe collections built by collections

3.java.util.concurrent

CopyOnWriteArrayList, CopyOnWriteArraySet, concurrenthashmap (recommended)

Posted by infyportalgroup on Fri, 19 Nov 2021 04:43:34 -0800