Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to use StampedLock, a high-performance tool to solve thread hunger

2025-03-01 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/03 Report--

This article introduces the knowledge of "how to use StampedLock, a high-performance tool to solve thread hunger". In the operation of practical cases, many people will encounter such a dilemma, so let the editor lead you to learn how to deal with these situations. I hope you can read it carefully and be able to achieve something!

Characteristics

It is originally designed as an internal tool class to develop other thread-safe components to improve system performance, and the programming model is more complex than ReentrantReadWriteLock, so it is easy to use deadlocks or thread safety and other inexplicable problems.

There are three modes of accessing data:

Writing (exclusive write lock): the writeLock method causes threads to block waiting for exclusive access, which is comparable to the write lock mode of ReentrantReadWriteLock, where only one and only one writer thread acquires lock resources at a time.

Reading (pessimistic read lock): readLock method, which allows multiple threads to acquire pessimistic read locks at the same time, pessimistic read locks and exclusive write locks are mutually exclusive, and share with optimistic reads.

Optimistic Reading (optimistic reading): it needs to be noted here that it is optimistic reading and is not locked. That is, there is no CAS mechanism and no threads are blocked. TryOptimisticRead returns a non-zero postmark (Stamp) only if it is not currently in Writing mode. If the write mode thread does not acquire locks after obtaining optimistic reads, then true is returned in the method validate, allowing multiple threads to acquire optimistic reads and read locks. It also allows a writer thread to acquire a write lock.

Support for mutual conversion of read-write locks

ReentrantReadWriteLock can be degraded to a read lock when a thread acquires a write lock, but not vice versa.

StampedLock provides the function of converting between read lock and write lock, which makes this class support more application scenarios.

Matters needing attention

Hongmeng official Strategic Cooperation to build HarmonyOS Technology Community

StampedLock is a non-reentrant lock. If the current thread has acquired the write lock, it will deadlock if it is acquired again.

Do not support the Conditon condition to wait for the thread

After StampedLock's write lock and pessimistic read lock are successfully locked, a st will be returned and the stamp will need to be passed in when unlocking.

Explain in detail the performance improvement brought about by optimistic reading

Then why is StampedLock better than ReentrantReadWriteLock?

The key is the optimistic reading provided by StampedLock. We know that ReentrantReadWriteLock supports multiple threads to acquire read locks at the same time, but when multiple threads read at the same time, all write threads are blocked.

The optimistic read of StampedLock allows one writer thread to acquire a write lock, so it does not block all write threads, that is, when there are more reads and less writes, the writer thread has the opportunity to acquire the write lock, which reduces the problem of thread hunger and greatly improves the throughput.

Here you may have a question, even allow multiple optimistic reads and a first thread to enter the critical resource operation at the same time, what if the read data may be wrong?

Yes, optimistic reading does not guarantee that the data read is up-to-date, so when reading data to a local variable, you need to use lock.validate (stamp) to verify whether it has been modified by the writer thread. If so, you need to apply a pessimistic read lock, and then re-read the data to the local variable.

At the same time, because optimistic reading is not a lock, there is no context switching caused by thread awakening and blocking, and the performance is better.

In fact, it is similar to the "optimistic lock" of the database, and its implementation idea is very simple. Let's give an example of a database.

A numeric version number field version is added to the table product_doc of the production order, and the version field is incremented by 1 each time the product_doc table is updated.

Select id,..., version from product_doc where id = 123

The update is performed only when the update matches the version.

Update product_doc set version = version + 1 Jing. Where id = 123 and version = 5

The optimistic lock of the database is to check out the version when querying and using the version field to verify when updating. If the equality indicates that the data has not been modified, the read data is safe.

The version here is similar to StampedLock's Stamp.

Use the example

Imitate to write one to save user id and user name data in the shared variable idMap, and provide put method to add data, get method to obtain data, and putIfNotExist to get data from map first, if not, then simulate to query data from database and put it into map.

Public class CacheStampedLock {/ * shared variable data * / private final Map idMap = new HashMap (); private final StampedLock lock = new StampedLock (); / * add data, exclusive mode * / public void put (Integer key, String value) {long stamp = lock.writeLock (); try {idMap.put (key, value) } finally {lock.unlockWrite (stamp);}} / * read data, read-only method * / public String get (Integer key) {/ / 1. Try to read data through optimistic read mode, non-blocking long stamp = lock.tryOptimisticRead (); / / 2. Read data to the current thread stack String currentValue = idMap.get (key); / / 3. Check whether it has been modified by other threads. True means it has not been modified, otherwise you need to add pessimistic read lock if (! lock.validate (stamp)) {/ / 4. Put a pessimistic read lock on and re-read the data to the current thread local variable stamp = lock.readLock (); try {currentValue = idMap.get (key);} finally {lock.unlockRead (stamp);}} / / 5. If the verification is passed, the data return currentValue will be returned directly. } / * if the data does not exist, read from the database and add it to the map. Lock upgrade using * @ param key * @ param value can be understood as data read from the database, assuming that the read lock will not be acquired for null * @ return * / public String putIfNotExist (Integer key, String value) {/ / You can also call the get method directly and use the optimistic read long stamp = lock.readLock () String currentValue = idMap.get (key); / / if the cache is empty, attempt to read data from the database and write to the cache try {while (Objects.isNull (currentValue)) {/ / attempt to upgrade the write lock long wl = lock.tryConvertToWriteLock (stamp) / / failed to upgrade the write lock for 0 successfully if (wl! = 0L) {/ / simulate reading data from the database and writing to the cache stamp = wl; currentValue = value; idMap.put (key, currentValue); break } else {/ / upgrade failed, release the read lock and write lock, and try lock.unlockRead (stamp) again through the loop; stamp = lock.writeLock () } finally {/ / release the last lock lock.unlock (stamp);} return currentValue;}}

In the above use example, what needs to be noticed are the get () and putIfNotExist () methods, the first of which uses optimistic reads so that reads and writes can be executed concurrently, and the second uses a programming model that converts read locks into write locks, queries the cache first, reads data from the database when it does not exist and adds it to the cache.

When using optimistic reading, be sure to follow a fixed template, otherwise it is easy to produce bug. Let's summarize the templates of optimistic reading programming model:

Public void optimisticRead () {/ / 1. Non-blocking optimistic read mode to get version information long stamp = lock.tryOptimisticRead (); / / 2. Copy the shared data to the thread local stack copyVaraibale2ThreadMemory (); / / 3. Verify whether the data read by optimistic read mode has been modified if (! lock.validate (stamp)) {/ / 3.1 check failed, stamp = lock.readLock (); try {/ / 3.2 copy shared variable data to local variable copyVaraibale2ThreadMemory () } finally {/ / release read lock lock.unlockRead (stamp);}} / / 3.3 check passed, using data from the thread's local stack for logical operation useThreadMemoryVarables ();}

Use scenarios and considerations

StampedLock has good performance for high concurrency scenarios with more reads and less writes. We can use StampedLock instead of ReentrantReadWriteLock to solve the problem of "hunger" of writer threads through optimistic read mode. But it should be noted that the function of StampedLock is only a subset of ReadWriteLock, so there are still a few places to pay attention to when using it.

Hongmeng official Strategic Cooperation to build HarmonyOS Technology Community

StampedLock is a non-reentrant lock. You must pay attention to it when using it.

Pessimistic read and write locks do not support the condition variable Conditon. You should pay attention to this feature when you need it.

If a thread blocks on readLock () or writeLock () of StampedLock, calling the interrupt () method of the blocking thread at this time will cause the CPU to soar. Therefore, do not invoke interrupt operations when using StampedLock, and be sure to use interruptible pessimistic read locks readLockInterruptibly () and write locks writeLockInterruptibly () if you need to support interrupt functionality. This rule must be remembered clearly.

Principle analysis

StapedLock local variable

We find that it does not implement synchronization logic by defining inner classes inheriting AbstractQueuedSynchronizer abstract classes and then subclasses implementing template methods like other locks. However, the implementation idea is similar, CLH queues are still used to manage threads, and the state of the lock is identified by synchronizing the status value state.

Many variables are defined internally, and the purpose of these variables is the same as ReentrantReadWriteLock, dividing the state into bitwise segments, and operating on state variables through bit operations to distinguish the synchronous state.

For example, the write lock uses the eighth bit 1 to indicate the write lock, and the read lock uses 0-7 bits, so the number of threads acquiring the read lock is generally 1-126. after that, the readerOverflow int variable will be used to save the number of threads exceeded.

Spin optimization

The multicore CPU is also optimized. NCPU obtains the number of cores. When the number of cores exceeds 1, the thread acquires lock retry and queue money retry all have spin operation. It is mainly judged by some internally defined variables, as shown in the figure.

Waiting queue

The nodes of the queue are defined by WNode, as shown in the figure above. The node waiting for the queue is simpler than AQS, and there are only three states:

0: initial statu

-1: waiting

Cancel

There is also a field cowait that points to a stack through which the reader thread is saved. The structure is shown in the figure

WNode

At the same time, two variables are defined to point to the head node and the tail node respectively.

/ * * Head of CLH queue * / private transient volatile WNode whead; / * * Tail (last) of CLH queue * / private transient volatile WNode wtail

Another point to pay attention to is cowait, which stores all read node data, using header insertion.

The data when the read and write threads compete to form a waiting queue is shown in the following figure:

Queue

Acquire write lock

Public long writeLock () {long s, next; / / bypass acquireWrite in fully unlocked case only return (s = state) & ABITS) = = 0L & & U.compareAndSwapLong (this, STATE, s, next = s + WBIT)? Next: acquireWrite (false, 0L);}

Acquire the write lock, and if the acquisition fails, the build node is queued and the thread is blocked. Note that this method does not respond to interrupts, and writeLockInterruptibly () is called if interrupts are needed. Otherwise, it will cause the problem of high CPU usage.

(s = state) & ABITS identity read lock and write lock are not used, then directly execute the U.compareAndSwapLong (this, STATE, s, next = s + WBIT) CAS operation to set the eighth bit to 1, indicating that the write lock is occupied successfully. If CAS fails, call acquireWrite (false, 0L) to join the waiting queue and block the thread.

In addition, the acquireWrite (false, 0L) method is very complex, using a large number of spin operations, such as spin queuing.

Acquire read lock

Public long readLock () {long s = state, next; / / bypass acquireRead on common uncontended case return ((whead = = wtail & & (s & ABITS) < RFULL & & U.compareAndSwapLong (this, STATE, s, next = s + RUNIT)? Next: acquireRead (false, 0L);}

Key steps to obtain read lock

(whead = = wtail & & (s & ABITS) < RFULL if the queue is empty and the number of read lock threads does not exceed the limit, the read lock is successfully obtained by modifying the state flag in the way of U.compareAndSwapLong (this, STATE, s, next = s + RUNIT)) CAS.

Otherwise, call acquireRead (false, 0L) to attempt to use spin to acquire the read lock, and if not, enter the waiting queue.

AcquireRead

When thread An acquires the write lock and thread B acquires the read lock, when the acquireRead method is called, the blocking queue is added and thread B is blocked. The interior of the method is still very complex, and the general process is as follows:

Hongmeng official Strategic Cooperation to build HarmonyOS Technology Community

If the write lock is not occupied, an attempt is made to acquire the read lock immediately, and if the status is changed to successful through CAS, it will be returned directly.

If the write lock is occupied, the current thread is wrapped as a WNode read node and inserted into the waiting queue. If it is a write thread node, it is placed directly at the end of the queue, otherwise it is placed on the stack pointed to by the WNode cowait of the read thread at the end of the queue. The stack structure is the way to insert data by head insertion, and finally wake up the read node, starting from the top of the stack.

Whether unlockRead releases the read lock or unlockWrite releases the write lock, the overall process is basically through the CAS operation. After the state is modified successfully, the release method is called to wake up the successor node thread of the head node waiting for the queue.

If you want to set the wait state of the header node to 0, indicate that the successor node is about to wake up.

The wake-up successor node acquires the lock through CAS, and if it is a read node, it wakes up all the stack read nodes pointed to by the cowait lock.

Release the read lock

UnlockRead (long stamp) if the passed stamp is the same as the stamp held by the lock, the non-exclusive lock is released. The internal state is modified successfully through spin + CAS. Before modifying the state, it is determined whether the limit of thread reading is exceeded. If it is less than the limit, the state synchronization status is modified through CAS, and then the release method is called to wake up the successor node of the whead.

Release the write lock

UnlockWrite (long stamp) if the passed stamp is the same as the stamp held by the lock, the write lock is released, the whead is not empty, and the current node state status! = 0, the release method is called to wake up the successor node thread of the header node.

Summary

StampedLock can not completely replace ReentrantReadWriteLock. In the scenario of reading more and writing less, because of the optimistic read mode, a writer thread is allowed to acquire a write lock, which solves the problem of writer thread hunger and greatly improves the throughput.

When using optimistic reading, you need to pay attention to writing according to the programming model template, otherwise it is easy to cause deadlocks or unexpected thread safety problems.

It is not a reentrant lock and does not support the condition variable Conditon. And when a thread blocks on readLock () or writeLock (), calling the interrupt () method of the blocking thread causes the CPU to soar. If you need to interrupt a thread, be sure to call pessimistic read locks readLockInterruptibly () and write locks writeLockInterruptibly ().

In addition, the rules for waking up threads are similar to AQS, except that when the node awakened by StampedLock is a read node, it will wake up all read nodes of the stack pointed to by the cowait lock of this read node, but wake up and insert in the opposite order.

This is the end of the content of "how to use StampedLock, a high-performance tool to solve thread hunger". Thank you for reading. If you want to know more about the industry, you can follow the website, the editor will output more high-quality practical articles for you!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report