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

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

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

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

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

insert image description here

The interface and class given before are only the parent class of all locks. Then, according to the actual use of different juc lock mechanisms, the following common lock processing classes are provided: ReentrantLock mutex, ReadWriteLock read-write lock, Condition control Queues, LockSupport blocking primitives, Semaphore semaphores, CountDownLatch locks, CyelicBarrier fences, Exchanger switches, CompletableFuture thread callbacks; the fundamental reason why a series of lock processing tools are re-provided in juc ​​is that the original java Although the lock mechanism (synchronized) can provide a secure access mechanism for data, its disadvantages are also very obvious. All thread objects can only enjoy one lock.

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

[ Core concept of fair lock ]
➣ AbstractQueuedSynchronizer: It is an abstract class that manages "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: A 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 obtain a lock fairly according to the first-come-first-served rule of waiting threads through the CLH; a non-fair lock means that when a thread wants to acquire a lock, it will directly acquire the lock regardless of the CLH waiting queue.
➣ Shared locks: locks that can be owned by multiple threads at the same time and can be shared;
➣ 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 the local variable, it continuously trains 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 exchange method, which is an atomic operation method; that is, the data operated by CAS is performed atomically.

[ CLH lock ---- solve the deadlock problem ]

insert image description here

Exclusive lock: ReentrantLock
➣ ReentrantLock is a reentrant mutex lock, also known as "exclusive lock".
➣ ReentrantLock locks can only be held by one thread lock at the same point in time; reentrant means that the lock 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. Other threads need to wait before acquiring the lock.
public class ReentrantLock extends Object implements Lock, Serializable
ReentrantLock is divided into fair lock and unfair lock, and the enabling of these two locks is also very easy to control, because the construction method provided on this class: • No parameter construction (
non- Fair lock, NonfairSync): public ReentrantLock();
• Constructed with parameters: public ReentrantLock(Boolean fair);
①fair = true; means fair lock, FairSync;
②fair = false; means unfair lock, NonfairSync;

**Example:** Define a multi-threaded ticketing handler

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestDemo {
    
    
    public static void main(String[] args) throws Exception {
    
    
        Ticket ticket = new Ticket() ; // 多个线程要共享同一个数据资源
        for (int x = 0 ; x < 6 ; x ++) {
    
    
            new Thread(() -> {
    
    
                while (true) {
    
    
                    ticket.sale();     // 卖票处理
                }
            }).start();
        }
    }
}
class Ticket {
    
    
    private Lock myLock = new ReentrantLock();
    private int count = 100; // 一共10张票
    public void sale() {
    
     // 进行卖票处理
        myLock.lock(); // 进入到阻塞状态,一直到unlock执行后解除阻塞
        try {
    
    
            if (this.count > 0) {
    
    
                System.out.println(Thread.currentThread().getName()
                        + "卖票,ticket = " + this.count --);
            }
        } finally {
    
    
            myLock.unlock();   // 不管最终如何结果一定要进行解锁
        }
    }
}

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 will be considered when using lock() for locking:
insert image description here

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

[ Read-write lock: ReadWriteLock ]
The so-called read-write lock refers to two locks, one "write lock" when data is written, and one "read lock" when data is read. Obviously, write locks must implement thread-safe synchronous processing operations, while read locks can be read by multiple objects.
➣ Divided into read locks and write locks, multiple read locks are not mutually exclusive (shared locks), read locks and write locks are mutually exclusive, controlled by jvm itself, developers only need to install the corresponding locks; ➣ ReentrantReadWriteLock will
use 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 exclude any WriteLock when it works, and WriteLock is completely exclusive. This feature is the most important, because for data structures with high read frequency and relatively low write, use This kind of lock synchronization mechanism can increase the amount of concurrency;
insert image description here

Write a bank deposit program below. Now there are 10 people depositing in your bank account. The deposit must use an exclusive lock (write lock), and when reading, all threads can read, and a shared lock (read lock) should be used. Lock). In the ReadWriteLock interface, you can find the following two methods to obtain locks:
obtain a write lock: public Lock writeLock();
obtain a read lock: public Lock readLock();
example : implement the processing operation of a read-write lock

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
 
public class TestDemo {
    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++) { // 设置两个写线程
            new Thread(() -> {
                for (int y = 0; y < money.length; y++) {
                    account.saveMoney(money[y]);
                }
            }, "存款用户-" + x).start();
        }
        for (int x = 0; x < 10; x++) { // 设置十个读线程
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName()
                        + "、查账,账户名:" + account.getName() + "、资产总额:"
                        + account.loadMoney());
            },"收款人-" + x).start();
        }
    }
}
 
class Account {
    private String name; // 开户名
    private double asset = 10.0; // 银行资产
    // 读写分离
    private ReadWriteLock rwLock = new ReentrantReadWriteLock();
    public Account(String name, double asset) {
        this.name = name;
        this.asset = asset;
    }
    // 进行存款处理
    public boolean saveMoney(double money) {
        this.rwLock.writeLock().lock(); // 对写入数据进行锁定处理
        String tname = Thread.currentThread().getName();
        try {
            System.out.println("【(" + tname + ")存款-BEFORE】存款金额:" + money);
            TimeUnit.SECONDS.sleep(1);
            if (money > 0.0) { // 如果要存款肯定是要有钱
                this.asset += money;
                return true; // 存款成功
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("【("+ tname + ")存款-AFTER】总金额:" + this.asset);
            this.rwLock.writeLock().unlock(); // 进行解锁处理
        }
        return false;
    }
 
    public String getName() {  return this.name; }
 
    public double loadMoney() { // 返回当前的资金
        try {
            this.rwLock.readLock().lock();
            return this.asset;
        } finally { this.rwLock.readLock().unlock();  }
    }
}

insert image description here

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

[ Precise control of locks: Condition ]
I have been exposed to basic locks before, but there is also an interface Condition when processing. This interface can be created by the user to create a lock object.
➣ Condition is used to control the lock more precisely. 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 used in conjunction with "synchronization lock"/"shared lock".
insert image description here
insert image description here

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 TestDemo1 {
    
    
    public static String msg = null ;  // 设置一个字符串
    public static void main(String[] args) throws Exception {
    
    
        // 实例化Lock接口对象
        Lock myLock = new ReentrantLock();
        // 创建一个新的Condition接口对象
        Condition condition = myLock.newCondition();
        // 如果现在不进行锁定,那么Condition无法执行等待处理机制,会出现“IllegalMonitorStateException”
        myLock.lock(); // 现在是在主线程之中执行了一个lock()处理
        try {
    
    
            new Thread(()->{
    
    
                myLock.lock() ;
                try {
    
    
                    msg = "www.baidu.com" ;
                    condition.signal(); // 唤醒等待的Condition
                } finally {
    
    
                    myLock.unlock();
                }
            }) .start();
            condition.await(); // 线程等待
            System.out.println("****主线程执行完毕,msg = " +msg);
        } finally {
    
    
            myLock.unlock();   // 解除阻塞状态
        }
    }
}

Compared with the previous Object, the only difference is that the explicit synchronized keyword is not visible now, and instead of synchronized are the lock() and unlock() methods in the Lock interface, which can be used in the blocked state (synchronized state) The await() and signal() in the Condition interface perform operations of waiting and waking up. After clarifying the basic use of Condition, let’s implement a slightly simpler and more interesting program. For array operation classes, it actually has a bigger role, which can be used as a data buffer operation.
insert image description here

Example: Implement 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 TestDemo2 {
    
    
    public static void main(String[] args) throws Exception {
    
    
        DataBuffer<String> buffer = new DataBuffer<String>() ;
        for (int x = 0 ; x < 3 ; x ++) {
    
       // 创建三个写线程
            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);
                }
            },"生产者-" + x).start();
        }
        for (int x = 0 ; x < 5 ; x ++) {
    
       // 创建五个读线程
            new Thread(()->{
    
    
                while(true) {
    
    
                    try {
    
    
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    System.out.println("【("+Thread.currentThread().getName()+")CONSUMER】" + buffer.get());
                }
            },"消费者-" + x).start();
        }
    }
}
 
// 进行数据的缓冲的操作控制,该缓冲可以保存各种数据类型
class DataBuffer<T> {
    
    
    // 该类之中保存的数组的长度个数为5
    private static final int MAX_LENGTH = 5 ;
    // 定义一个数组进行全部数据的保存控制
    private Object [] data = new Object [MAX_LENGTH] ;
    // 创建数据锁
    private Lock myLock = new ReentrantLock();
    // 数据保存的Condition控制
    private Condition putCondition = myLock.newCondition() ;
    // 数据取得的Condition控制
    private Condition getCondition = myLock.newCondition() ;   
    private int putIndex = 0 ; // 保存数据的索引
    private int getIndex = 0 ; // 读取数据的索引
    private int count = 0 ;    // 当前保存的元素个数
    public T get() {
    
               // 根据缓冲读取数据
        Object takeObject = null ;
        this.myLock.lock();
        try {
    
    
            if (this.count == 0) {
    
     // 没有写入
                this.getCondition.await(); // 读取的线程要进行等待
            }
            // 读取指定索引数据
            takeObject = this.data[this.getIndex ++] ; 
            if (this.getIndex == MAX_LENGTH) {
    
    
                this.getIndex = 0 ;    // 重新开始读
            }
            this.count -- ;// 因为读取了一个数据之后,现在需要减少个数
            this.putCondition.signal(); // 告诉写线程可以写入
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            this.myLock.unlock();
        }
        return (T) takeObject ;
    }
    // 进行缓冲数据的写入处理
    public void put(T t) {
    
    
        // 进入独占锁定状态
        this.myLock.lock(); 
        try {
    
    
            // 保存的数据量已经满了
            if (this.count == MAX_LENGTH) {
    
        
                // 暂时先别进行数据保存了
                this.putCondition.await(); 
            }
            // 保存当前的数据
            this.data[this.putIndex ++] = t ;  
            // 现在索引已经写满
            if (this.putIndex == MAX_LENGTH) {
    
     
                // 重置数组操作的索引脚标
                this.putIndex = 0 ;    
            }
            this.count ++ ;     // 保存的个数需要做一个追加
            this.getCondition.signal(); // 唤醒消费线程
            System.out.println("【(" + Thread.currentThread().getName() + ")写入缓冲-put()】" + t);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            this.myLock.unlock(); // 不管如何最终一定要进行解锁
        }
    }
}

insert image description here

For the implementation of the producer and consumer models, in addition to explaining the multi-threaded basic implementation model, you can also use the above model to use Lock and Condition to precisely control the lock.

[ Blocking primitive: LockSupport ]
java.util.concurrent.locks.LockSupport is an independent class. The main function of this class is to solve the suspend() (suspend thread) and resume()( Reply to run) method, these two methods are suspected of deadlock in essence, so they have been listed as a method that is not recommended since JDK1.4, but after the JDK develops the JUC architecture, considering the JUC The various implementation mechanisms in the architecture began to try to restore the previously abandoned operations, so there was a LockSupport class, which has two methods:
suspend: public static void park(Object blocker);
restore: public static void unpark(Thread thread);
example: observe suspend and resume execution

import java.util.concurrent.locks.LockSupport;
public class TestDemo3 {
    
    
    public static String msg = null ;  // 设置一个字符串
    public static void main(String[] args) throws Exception {
    
    
        // 获得当前的线程操作类
        Thread mainThread = Thread.currentThread();
        new Thread(()->{
    
    
            try {
    
    
                msg = "www.baidu.com" ;
            } finally {
    
      // 解锁关起状态
                LockSupport.unpark(mainThread);
            }
        }) .start();
        LockSupport.park(mainThread);
        System.out.println("********** 主线程执行完毕,msg="+msg);
    }
}

These processing methods are actually perfect for the implementation mechanism of the original threading model. The advantage of using these classes is that thread synchronization locks can be easily and simply implemented, and problems caused by deadlocks can be avoided.

Guess you like

Origin blog.csdn.net/kongfanyu/article/details/109838479