In-depth analysis of JUC thread framework - 02, thread synchronization lock

     The core problem solved by juc's development architecture is the problem of concurrent access and data security operation. When concurrent access is performed, if the lock is not properly controlled, it will cause blocking problems such as deadlock. In order to solve such a defect, juc re-engineered Designed for the concept of locks.

[JUC lock mechanism] ➣  JUC
lock mechanism includes the following components:
   ➣ Core interface: Lock, ReadWriteLock;
   ➣ AQS abstract class:
• AbstractOwnableSynchronizer (exclusive lock);
Locks and related
 synchronizers (semaphores, events, etc.) provide a framework) ;
• AbstractQueuedLongSynchronizer (64-bit synchronizer)
   ➣ Tools: ReentrantLock mutex, ReadWriteLock, Condition control queue,
           LockSupport blocking primitive, Semaphore Semaphore, CountDownLatch latch,
           CyclicBarrier fence, Exchanger switch, CompletableFuture thread return;

[juc lock mechanism (java.util.concurrent.locks)]


    The interfaces and classes given before are only the parent classes of all locks, so the following common lock processing classes will be provided according to the actual use of different juc lock mechanisms: ReentrantLock mutex lock, ReadWriteLock read-write lock, Condition control Queue, LockSupport blocking primitive,
Semaphore semaphore, CountDownLatch latch, CyelicBarrier fence,
Exchanger switch, CompletableFuture thread callback;

    The fundamental reason why a series of lock processing tool classes are re-provided in juc ​​is that although java's original lock mechanism (synchronized) can provide a safe access mechanism for data, its shortcomings are also very obvious. The thread object can only enjoy one lock.

[java.util.concurrent lock overview]
➣ java.util.concurrent.lock provides basic support for locks;
➣ Lock interface: supports lock rules with different semantics (inrush, fairness, etc.).
   ➣ The so-called different semantics means that the lock can have "fair mechanism lock", "unfair mechanism lock", "reentrant lock", etc.;
      • "Fair mechanism": refers to "the mechanism for different threads to acquire locks is "Fair";
      • "Unfair Mechanism": It means that "the mechanism by which different threads acquire locks is unfair";
      • "Reentrant lock": It means that the same lock can be acquired multiple times by a thread, and the maximum number of reentrant locks The role is to avoid deadlocks.
➣ The ReadWriteLock interface defines, in a similar manner to Lock, some locks that can be shared by readers and exclusive by writers.
➣ Condition: The interface describes the condition variables that may be associated with the lock (the wait() method in the Object class is used similarly).

[Fair lock core concept]
➣ AbstractQueuedSynchronizer: It is an abstract class for managing "locks" in java. Many public methods of locks
   are implemented in this class. AbstractQueuedSynchronizer is an exclusive lock (for example, ReentrantLock)
➣ AbstractQueuedSynchronizer category:
   ➣ Exclusive lock: The lock can only be occupied by one thread lock at a time point. According to the lock acquisition mechanism, it
      is divided into "fair lock" and "unfair lock" . A fair lock is to acquire a lock fairly according to the first -come, first
      -served rule of waiting for a thread through CLH; a non-fair lock, when a thread wants to acquire a lock, it will
      The lock is acquired directly regardless of the CLH waiting queue.
   ➣ Shared locks: locks that can be owned and shared by multiple threads at the same time;
➣ CLH queues (Craig, Landin, and Hagersten locks): CLH locks are also scalable,
       high-performance, and fair spin locks based on linked lists , the application thread only spins on local variables, it continuously rotates the state of the predecessor,
       and ends the sub-spin if it finds that the predecessor has released the lock.

➣ CAS method (Compare And Swap): Compare And Swap method, which is an atomic operation method; that is, the data manipulated by CAS is performed in an atomic manner.

【 CLH lock ---- solve the deadlock problem 】


[Exclusive lock: ReentrantLock]
➣ ReentrantLock is a reentrant mutex, also known as "exclusive lock".
➣ ReentrantLock locks can only be held by one thread lock at the same point in time; reentrant means that
  ReentrantLock locks can be acquired multiple times by a single thread.
➣ ReentrantLock is divided into "fair lock" and "unfair lock". The difference between them is reflected in the fairness of the lock acquisition mechanism
  and the execution speed.
➣ ReentrantLock manages all threads that acquire the lock through a FIFO waiting queue.
   ReentrantLock is an exclusive lock. After acquiring the lock, all its operations are exclusive to the thread, and other threads need to wait before acquiring the lock.
public class ReentrantLock

class Object
implements Lock, Serializable
   ReentrantLock is divided into fair lock and unfair lock, and the enabling of these two kinds of locks is also very easy to control, because the construction methods provided on this class
    : Fair lock, NonfairSync): public ReentrantLock();
    • Constructed with parameters: public ReentrantLock(Boolean fair);
       |- fair = true; means fair lock, FairSync ;
       |- fair = false; means non-fair lock, NonfairSync ;
 

Example: Defining a multi-threaded ticket-selling handler

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MLDNTestDemo {
    public static void main(String[] args) throws Exception {
        Ticket ticket = new Ticket() ; // Multiple threads share the same data resource
        for (int x = 0 ; x < 6 ; x ++) {
            new Thread(() -> {
                while (true) {
                    ticket.sale(); // ticket processing
                }
            }).start();
        }
    }
}
class Ticket {
    private Lock myLock = new ReentrantLock();
    private int count = 100; // a total of 10 tickets

    public void sale() { // handle ticket sales
        myLock.lock(); // Enter the blocking state until unblocked after unlock is executed
        try {
            if (this.count > 0) {
                System.out.println(
                        Thread.currentThread().getName()
                        + "卖票,ticket = " + this.count --);
            }
        } finally {
            myLock.unlock(); // must be unlocked regardless of the final result
        }
    }
}

The current code is easier than using synchronized directly, and the lock processing mechanism is more intuitive. It can be found from the source code that two situations are considered when using lock() for locking:


When performing fair lock processing, each time a thread object is locked, it will use the "acquire(1)" method to indicate it. When unlocking, it will use a "sync.release(1)" release method, and 1 means release one.

[Read-write lock: ReadWriteLock]
The so-called read-write lock refers to two locks, one "write lock" when writing data, and one "reading lock" when reading data. Obviously, write locks must achieve thread-safe synchronization operations, while read locks can be acquired by multiple objects.
➣ Divided into read locks and write locks, multiple read locks are not mutually exclusive (shared locks), and read locks and write locks are mutually exclusive, which are controlled by JVM itself, and developers only need to lock the corresponding lock;
➣ ReentrantReadWriteLock Two locks are used to solve the problem, a read lock (multiple threads can read at the same time) and a write lock (single thread writes).

➣ ReadLock can be held by multiple threads and excludes any WriteLock when acting, while WriteLock is completely mutually exclusive. This feature is the most important, because for data structures with high read frequency and relatively low write frequency, use This kind of lock synchronization mechanism can improve the concurrency;


      Let's write a bank deposit program. Now there are 10 people depositing money into your bank account. The storage must use an exclusive lock (write lock), and when reading, all threads can read it, and a shared lock (read lock) should be used. Lock).
       In the ReadWriteLock interface, it can be found that there are the following two methods to obtain the lock:
              to obtain a write lock: public Lock writeLock();
              to obtain a read lock: public Lock readLock();

Example: Implementing read-write lock processing operations

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class MLDNTestDemo {
    public static void main(String[] args) throws Exception {
        Account account = new Account("小林子", 15.0);
        double money[] = new double[] { 5.0, 300.0, 5000.0, 50000.0, 1000.0 };
        for (int x = 0; x < 2; x++) { // set up two write threads
            new Thread(() -> {
                for (int y = 0; y < money.length; y++) {
                    account.saveMoney(money[y]);
                }
            }, "Deposit User-" + x).start();
        }
        for (int x = 0; x < 10; x++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName()
                        + ", check accounts, account name: " + account.getName() + ", total assets: "
                        + account.loadMoney());
            },"Payee-" + x).start();
        }
    }
}

class Account {
    private String name; // account name
    private double asset = 10.0; // bank asset
    // read and write separation
    private ReadWriteLock rwLock = new ReentrantReadWriteLock();
    public Account(String name, double asset) {
        this.name = name;
        this.asset = asset;
    }
    // do deposit processing
    public boolean saveMoney(double money) {
        this.rwLock.writeLock().lock(); // lock the write data
        try {
            System.out.println("【("
                    + Thread.currentThread().getName()
                    + ") Deposit-BEFORE】Deposit Amount: " + money);
            TimeUnit.SECONDS.sleep(1);
            if (money > 0.0) { // If you want to deposit money, you must have money
                this.asset += money;
                return true; // deposit successful
            }
        } catch (Exception e) {
            e.printStackTrace ();
        } finally {
            System.out.println("【("
                    + Thread.currentThread().getName()
                    + ") Deposit-AFTER] Total Amount: " + this.asset);
            this.rwLock.writeLock().unlock(); // Unlock processing
        }
        return false;
    }

    public String getName() {  return this.name; }

    public double loadMoney() { // return current money
        try {
            this.rwLock.readLock().lock();
            return this.asset;
        } finally { this.rwLock.readLock().unlock();  }
    }
}

      The processing speed of exclusive lock is slow, but it can ensure the security of thread data, while the processing speed of shared lock is fast, which is the lock processing mechanism for multiple threads, and this read-write processing relationship is the core implementation of the important class set ConcurrentHashMap Thought.

[Precise control of locks: Condition]
   Some basic locks have been touched before, but there is also an interface Condition when processing, which can be created by users to lock objects themselves.

➣ The role of Condition is to have more precise control over locks. The await() method in Condition is equivalent to the wait() method of Object, the signal() method in Condition is equivalent to the notify() method of Object, and the signalAll() method in Condition is equivalent to the notifyAll() method of Object. The difference is that the wait(), notify(), and notifyAll() methods in Object are bundled with "synchronized locks"/"shared locks".



Example: Observe the basic use of Condition

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MLDNTestDemo {
    public static String msg = null ; // set a string
    public static void main(String[] args) throws Exception {
        // Instantiate the Lock interface object
        Lock myLock = new ReentrantLock();
        // Create a new Condition interface object
        Condition condition = myLock.newCondition();
        // If the lock is not performed now, then the Condition cannot execute the waiting mechanism, and an "IllegalMonitorStateException" will occur
        myLock.lock(); // now a lock() is executed in the main thread
        try {
            new Thread(()->{
                myLock.lock() ;
                try {
                    msg = "www.baidu.com" ;
                    condition.signal(); // wake up the waiting Condition
                } finally {
                    myLock.unlock();
                }
            }) .start();
            condition.await(); // thread waiting
            System.out.println("****The main thread is executed, msg = " +msg);
        } finally {
            myLock.unlock(); // unblock state
        }
    }
}
       Compared with the previous Object, the only difference is that now there is no clear synchronized keyword, and instead of synchronized are the lock() and unlock() methods in the Lock interface, which can then be used in the blocking state (synchronized state). The await() and signal() in the Condition interface perform the operation processing of waiting and waking up.

      After the basic usage of Condition is clear, then a slightly simpler and interesting program is implemented below. In fact, it has a larger role for the array operation class, which can be used as a data buffer operation.


Example: Implementing data buffer control

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

public class MLDNTestDemo {
    public static void main(String[] args) throws Exception {
        DataBuffer<String> buffer = new DataBuffer<String>() ;
        for (int x = 0 ; x < 3 ; x ++) { // create three writer threads
            new Thread(()->{
                for (int y = 0 ; y < 2 ; y ++) {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace ();
                    }
                    buffer.put(Thread.currentThread().getName() + "写入数据,y = " + y);
                }
            },"Producer-" + x).start();
        }
        for (int x = 0 ; x < 5 ; x ++) { // create five reading threads
            new Thread(()->{
                while(true) {
                    try {
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
                        e.printStackTrace ();
                    }
                    System.out.println("【("+Thread.currentThread().getName()+")CONSUMER】" + buffer.get());
                }
            },"Consumer-" + x).start();
        }
    }
}

// Control the operation of data buffering, which can save various data types
class DataBuffer<T> {
    // The length of the array stored in this class is 5
    private static final int MAX_LENGTH = 5 ;
    // Define an array for all data storage control
    private Object [] data = new Object [MAX_LENGTH] ;
    // create data lock
    private Lock myLock = new ReentrantLock();
    // Condition control for data saving
    private Condition putCondition = myLock.newCondition() ;
    // Condition control for data acquisition
    private Condition getCondition = myLock.newCondition() ;   
    private int putIndex = 0 ; // save the index of the data
    private int getIndex = 0 ; // index of read data
    private int count = 0 ; // the number of elements currently saved
    public T get() { // read data from buffer
        Object takeObject = null ;
        this.myLock.lock();
        try {
            if (this.count == 0) { // not written
                // read thread to wait
                this.getCondition.await();
            }
            // Read the specified index data
            takeObject = this.data[this.getIndex ++] ;
            if (this.getIndex == MAX_LENGTH) {
                this.getIndex = 0 ; // start reading again
            }
            // Because after reading a data, now need to reduce the number
            this.count -- ;
            // Tell the writing thread to write
            this.putCondition.signal();
        } catch (Exception e) {
            e.printStackTrace ();
        } finally {
            this.myLock.unlock();
        }
        return (T) takeObject ;
    }
    // Write the buffered data
    public void put(T t) {
        // enter exclusive lock state
        this.myLock.lock();
        try {
            // The amount of saved data is full
            if (this.count == MAX_LENGTH) {    
                // Don't save the data for now
                this.putCondition.await();
            }
            // save current data
            this.data[this.putIndex ++] = t ;  
            // now the index is full
            if (this.putIndex == MAX_LENGTH) {
                // reset the index subscript of the array operation
                this.putIndex = 0 ;    
            }
            // The number of saved numbers needs to be appended
            this.count ++ ;    
            this.getCondition.signal(); // wake up the consuming thread
            System.out.println("【(" + Thread.currentThread().getName() + ")写入缓冲-put()】" + t);
        } catch (Exception e) {
            e.printStackTrace ();
        } finally {
            // In any case, it must be unlocked in the end
            this.myLock.unlock();
        }
    }
}

      For the implementation of the producer and consumer models, in addition to explaining the model of multi-threaded basic implementation, the above model can also be used for precise control of locks using Lock and Condition.

[Blocking primitive: LockSupport]
       java.util.concurrent.locks.LockSupport is an independent class. The main function of this class is to solve the suspend() (suspended thread), resume()( Reply to run) method, these two methods are inherently suspected of deadlock, so they have been listed as deprecated methods since JDK1.4, but after the JDK developed the JUC architecture, considering the JUC Various implementation mechanisms in the architecture, so I began to try to restore the previously abandoned operations, so I got the LockSupport class, which has two methods:
       Suspend: public static void park(Object blocker);

       恢复:public static void unpark(Thread thread);

Example: Watching Suspend and Resume Execution

import java.util.concurrent.locks.LockSupport;
public class MLDNTestDemo {
    public static String msg = null ; // set a string
    public static void main(String[] args) throws Exception {
        // Get the current thread operation class
        Thread mainThread = Thread.currentThread();
        new Thread(()->{
            try {
                msg = "www.baidu.com" ;
            } finally { // unlock the closed state
                LockSupport.unpark(mainThread);
            }
        }) .start();
        LockSupport.park(mainThread);
        System.out.println("********** Main thread execution completed, msg="+msg);
    }
}
       These processing methods are actually perfect for the realization mechanism of the original thread model. The advantage of using these classes is that thread synchronization locks can be implemented easily and simply, and problems caused by deadlocks can be avoided.




 







Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326002496&siteId=291194637