Concurrent container JUC-AQS synchronization component (3)

The implementation of ReentrantLock relies on the Java synchronizer framework AbstractQueuedSynchronizer (AQS). AQS uses an integer volatile variable (state) to maintain the synchronization state

ReentrantLock

There are two types of locks in java: Synchronized and locks provided in JUC.
ReentrantLock and Synchronized are both reentrant locks, which are essentially lock and unlock operations.

The difference between ReentrantLock and synchronized

Reentrancy : Both locks are reentrant, the difference is not big, a thread enters the lock, the counter is incremented by 1, and the lock can be released when it drops to 0

Lock realization : synchronized is realized based on JVM (users are hard to see and cannot understand its realization), and ReentrantLock is realized by JDK.
Performance difference : At the beginning, the performance of the two is much worse. When synchronized introduces a bias lock and a lightweight lock (spin lock), the performance of the two is not much different. 官方推荐synchronized(It is easier to write, and it is actually Borrowing the CAS technology of ReentrantLock, trying to solve the problem in the user mode to avoid the thread blocking caused by entering the kernel mode )
Function difference:

  1. Convenience : synchronized is more convenient, it is guaranteed by the compiler to lock and release. ReentrantLock needs to manually release the lock, so in order to avoid forgetting to manually release the lock and cause a deadlock, it is best to declare the release lock in finally.
  2. The fine-grained and flexible degree of lock , ReentrantLock is better than synchronized

Unique features of ReentrantLock

  1. ReentrantLock can be specified as a fair lock and unfair
    (fair lock: the thread that waits first obtains the lock first, and the default uses an unfair lock)
    synchronized can only be an unfair lock
  2. A Condition class is provided, which can group threads that need to be awakened.
    synchronized Either randomly wake up a thread, or wake up all
  3. Provide a thread mechanism that can interrupt waiting for a lock, lock.lockInterruptibly() is implemented. The
    implementation of ReentrantLock is a spin lock . By cyclically calling the cas self-adding operation, it avoids the thread entering the kernel state from blocking and
    synchronized and will not forget to release the lock.

ReentrantLock acquire lock release lock

ReentrantLock类图

Fair lock vs. unfair lock

int c = getState();// At the beginning of acquiring the lock, first read the volatile variable state
//Compared with nonfairTryAcquire(int acquires), the only difference is that the fair lock has an additional restriction when acquiring the synchronization state: hasQueuedPredecessors()
Fair lock vs. unfair lock
The hasQueuedPredecessors() method determines whether the current thread is the first in the synchronization queue. If it is, it returns true, otherwise it returns false.

   //即加入了同步队列中当前节点是否有前驱节点的判断
   //返回true,则表示有线程比当前线程更早地请求获取锁
   //因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

hasQueuedPredecessors () 方法
Release lock Sync: tryRelease()

protected final boolean tryRelease(int releases) {
    
    
            int c = getState() - releases;//读取state
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
    
    
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);// 释放锁的最后,写volatile变量state
            return free;
        }

The fair lock writes the volatile variable state at the end of the lock release, and reads the volatile variable first when acquiring the lock. int c = getState();
According to the happens-before rule of volatile (the write operation of a volatile variable occurs before the subsequent read operation of this volatile variable), the shared variable that is visible before the thread that releases the lock writes the volatile variable, and the thread that acquires the lock reads the same volatile The variable will immediately become visible to the thread that acquired the lock.

Memory semantics for fair and unfair locks

For unfair locks, as long as CAS successfully sets the synchronization state, it means that the current thread has acquired the lock, while fair locks are different.

When a fair lock and an unfair lock are released, a volatile variable state must be written at the end. When a
fair lock is acquired, first read the volatile variable. When
an unfair lock is acquired, first use CAS to update the volatile variable. This operation has both volatile read and volatile write. Memory semantics.

How does CAS have the memory semantics of volatile read and volatile write at the same time? The
compiler will not 读后面reorder any memory operations of volatile read and volatile ; the
compiler will not reorder any memory operations of volatile write and volatile 写前面.
Combining these two conditions means that in order to achieve the memory semantics of volatile read and volatile write at the same time, the compiler cannot reorder CAS and any memory operations before and after CAS.

To sum up:
Fair lock is to realize that multiple threads acquire locks in the order in which they apply for locks by synchronizing the queue, thereby realizing the characteristics of fairness.
When the unfair lock is locked, the queue waiting is not considered, and the lock is directly tried to obtain the lock, so it is the case that the lock is obtained after the application is applied.

Give up on synchronized?

ReentrantLock not only has all the functions of synchronized, but also has some features that cannot be realized by synchronized. In terms of performance, ReentrantLock is no worse than synchronized, so should we give up using synchronized? The answer is not to do this.

The lock class in the JUC package is a tool for advanced situations and advanced users, unless you have a particularly clear understanding of the advanced features of Lock and have a clear need, or there is clear evidence that synchronization has become scalable When the bottleneck occurs, otherwise we will continue to use synchronized. Compared with these advanced locking classes, synchronized still has some advantages. For example, synchronized cannot forget to release the lock. Also, when the JVM uses synchronized to manage lock requests and releases, the JVM can include lock information when generating thread dumps. This information is very valuable for debugging. They can identify the source of deadlocks and other abnormal behaviors.

Use of ReentrantLock

//创建锁:使用Lock对象声明,使用ReentrantLock接口创建
private final static Lock lock = new ReentrantLock();
//使用锁:在需要被加锁的方法中使用
private static void add() {
    
    
    lock.lock();//获取锁
    try {
    
    
        count++;
    } finally {
    
    
        lock.unlock();//释放锁
    }
}

Source code

//初始化方面:
//在new ReentrantLock的时候默认给了一个不公平锁
public ReentrantLock() {
    
    
    sync = new NonfairSync();
}
//加参数来初始化指定使用公平锁还是不公平锁
//fair=true时公平锁
public ReentrantLock(boolean fair) {
    
    
    sync = fair ? new FairSync() : new NonfairSync();
}

ReentrantLock function method

tryLock(): Acquire the lock only if the lock is not held by another thread at the time of the call.
tryLock(long timeout, TimeUnit unit): If the lock is not held by another thread within a given time and the current thread is not interrupted, then the lock is acquired.
lockInterruptbily: If the current thread is not interrupted, then the lock is acquired. If it is interrupted, an exception is thrown.
isLocked: Query whether the lock is held by any thread
isHeldByCurrentThread: Query whether the current thread is held in the locked state.
isFair: Determine if it is a fair lock
...

Condition related features:

hasQueuedThread(Thread): Query whether the specified thread is waiting to acquire this lock.
hasQueuedThreads(): Query whether any threads are waiting to acquire this lock.
getHoldCount(): Query the number of current threads holding the lock, that is, the number of calls to the Lock method
...

Use of Condition

Condition can operate thread wake-up very flexibly. The following is an example of thread waiting and wake-up, in which the log output sequence is marked with the 1234 serial number

public static void main(String[] args) {
    
    
    ReentrantLock reentrantLock = new ReentrantLock();
    Condition condition = reentrantLock.newCondition();//创建condition
    //线程1
    new Thread(() -> {
    
    
        try {
    
    
            reentrantLock.lock();
            log.info("wait signal"); // 1
            condition.await();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        log.info("get signal"); // 4
        reentrantLock.unlock();
    }).start();
    //线程2
    new Thread(() -> {
    
    
        reentrantLock.lock();
        log.info("get lock"); // 2
        try {
    
    
            Thread.sleep(3000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        condition.signalAll();//发送信号
        log.info("send signal"); // 3
        reentrantLock.unlock();
    }).start();
}

Output result:
1–wait signal
2–get lock
3–send signal
4–get signal

Explanation of the output process:
1. Thread 1 calls reentrantLock.lock(), the thread enters the AQS waiting queue, and outputs log No. 1.
Then, the awiat method is called, the thread is removed from the AQS queue, the lock is released, and directly added to the condition waiting Queue
3, thread 2 because thread 1 released the lock and got the lock, it outputs log
4, thread 2 executes condition.signalAll() to send a signal, and outputs log
5, the node of thread 1 in the condition queue receives the signal , Take it out of the condition queue and put it into the waiting queue of AQS. At this time, thread 1 is not awakened.
6. Thread 2 calls unlock to release the lock. Because there is only thread 1 in the AQS queue, AQS releases the lock in order from beginning to end to wake up thread 1
7. Thread 1 continues to execute, output log No. 4, and perform the unlock operation.

Read and write lock: ReentrantReadWriteLock

The exclusive lock allows only one thread to access at the same time (ReentrantLock is an exclusive lock).
The read-write lock can allow multiple reader threads to access at the same time, but when the writer thread accesses, all reader threads and other writer threads are blocked. The read-write lock maintains a pair of locks, a read lock and a write lock. By separating the read lock and the write lock, the concurrency is greatly improved compared to the general exclusive lock.

In addition to ensuring the visibility of write operations to read operations and the improvement of concurrency , read-write locks can simplify the programming of read-write interaction scenarios. Acquire the read lock during read operations, and acquire the write lock during write operations. When the write lock is acquired, subsequent (non-current write operation threads) read and write operations will be blocked . After the write lock is released, all operations continue to execute.

scenes to be used

public class LockExample3 {
    
    
    private final Map<String, Data> map = new TreeMap<>();
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();//读锁
    private final Lock writeLock = lock.writeLock();//写锁
    //加读锁
    public Data get(String key) {
    
    
        readLock.lock();
        try {
    
    
            return map.get(key);
        } finally {
    
    
            readLock.unlock();
        }
    }
    //加写锁  设置key对应的value,并返回旧的value
    public Data put(String key, Data value) {
    
    
        writeLock.lock();
        try {
    
    
            return map.put(key, value);
        } finally {
    
    
            writeLock.unlock();
        }
    }

    class Data {
    
    }
}

Bill lock: StempedLock

Write, read, and optimistic read
The state of a StempedLock is composed of two parts: version and mode . The lock acquisition method returns a number as a stamp, which uses the corresponding lock status to indicate and control related access. The number 0 means that no write lock is authorized to access, and it is divided into [pessimistic reading, optimistic reading] on the read lock.

Optimistic reading: Read more and write less. Optimistic people think that the chance of reading and writing at the same time is very small, so they should not be used pessimistically 完全的读取锁定. The program can check whether the changes have been written after reading, and then take corresponding measures.

use

//定义
private final static StampedLock lock = new StampedLock();
//需要上锁的方法
private static void add() {
    
    
    long stamp = lock.writeLock();
    try {
    
    
        count++;
    } finally {
    
    
        lock.unlock(stamp);
    }
}

Source code

class Point {
    
    
        private double x, y;
        private final StampedLock sl = new StampedLock();
        void move(double deltaX, double deltaY) {
    
    
            long stamp = sl.writeLock();
            try {
    
    
                x += deltaX;
                y += deltaY;
            } finally {
    
    
                sl.unlockWrite(stamp);
            }
        }

        //下面看看乐观读锁案例
        double distanceFromOrigin() {
    
     // A read-only method
            long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
            double currentX = x, currentY = y;  //将两个字段读入本地局部变量
            if (!sl.validate(stamp)) {
    
     //检查发出乐观读锁后同时是否有其他写锁发生?
                stamp = sl.readLock();  //如果没有,我们再次获得一个读悲观锁
                try {
    
    
                    currentX = x; // 将两个字段读入本地局部变量
                    currentY = y; // 将两个字段读入本地局部变量
                } finally {
    
    
                    sl.unlockRead(stamp);
                }
            }
            return Math.sqrt(currentX * currentX + currentY * currentY);
        }

        //下面是悲观读锁案例
        void moveIfAtOrigin(double newX, double newY) {
    
     // upgrade
            // Could instead start with optimistic, not read mode
            long stamp = sl.readLock();
            try {
    
    
                while (x == 0.0 && y == 0.0) {
    
     //循环,检查当前状态是否符合
                    long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁
                    if (ws != 0L) {
    
     //这是确认转为写锁是否成功
                        stamp = ws; //如果成功 替换票据
                        x = newX; //进行状态改变
                        y = newY;  //进行状态改变
                        break;
                    } else {
    
     //如果不能成功转换为写锁
                        sl.unlockRead(stamp);  //我们显式释放读锁
                        stamp = sl.writeLock();  //显式直接进行写锁 然后再通过循环再试
                    }
                }
            } finally {
    
    
                sl.unlock(stamp); //释放读锁或写锁
            }
        }
    }

to sum up

synchronized: JVM implementation can be monitored by some monitoring tools. When an unknown exception occurs, JVM will automatically help release the lock.
ReetrantLock, ReetrantReadWriteLock, and StempedLock are all object-level locks. In order to ensure that the lock must be released, it will be safer to put it in finally . StempedLock has greatly improved performance, especially when there are more and more reader threads.

How to choose a lock

1. When there are only a few competitors, use synchronized
2. There are many competitors but the trend of thread growth is predictable, use ReetrantLock

Guess you like

Origin blog.csdn.net/eluanshi12/article/details/85262018