Moment For Technology

Fine-grained lock implementations common in Java

Posted on June 23, 2022, 12:35 p.m. by Kim Hamilton
Category: The back-end Tag: java

The ReentrantLock class was briefly introduced in the previous article. After a preliminary understanding of the ReentrantLock class, let's take a look at several fine-grained lock implementations based on ReentrantLock.

In this example, we continue to use the synchronize keyword to synchronize the money from our account. For example, we switch gears and convert the money to a gift certificate for use in our system. Users pay a gift certificate each time they buy a feature in the system. Since fine-grained locking is to be implemented, it means that the operation of deducting gift certificates from different user accounts does not affect each other. It only needs to ensure that the operation of deducting gift certificates under the same account is thread safe, and only a small part of the code block of deducting gift certificates is deducted. For account verification, we can accept concurrent execution.

Segmented lock

Let's imagine a scenario where only a small percentage of our system's loyal users are willing to use our system for special features by purchasing gift certificates, and more users are just using our system's basic features. Next we need to consider two cases:

  • The ability for the same user to pay for a system at the same time through a gift voucher, i.e. the need for thread safety when deducting money
  • This is the number of concurrent paying users in the system, which is related to the number of fine-grained locks we need to create.

Based on the scenario we described above, it can be seen that in the first case, as long as there is gift certificate consumption, although it does not occur often, it certainly exists, so we need to control the deduction operation of gift certificate through fine-grained lock. The second case also exists, but since the base of paying users is not very large, it is very rare, which means the actual number of locks to be created at one time is not very large. Explain it a little bit here, because at the time of consumption coupon we must be to create a lock to ensure the safety of the thread, and the lock is binding and users, and at the same time for the same user will only create a lock, but at the same time if many users are consumption coupon, that this moment is how much how many users will need to create a lock.

Ok, through the above analysis, let's look at how segmented locking achieves fine-grained locking.

public class SegmentLockT {

    /** * Default number of pre-created locks. */
    private int DEFAULT_LOCK_COUNT = 20;

    private final ConcurrentHashMapInteger, ReentrantLock lockMap = new ConcurrentHashMap();

    public SegmentLock(a) {
        init(null.false);
    }

    public SegmentLock(Integer count, boolean isFair) {
        init(count, isFair);
    }

    private void init(Integer count, boolean isFair) {
        if(count ! =null count ! =0) {
            this.DEFAULT_LOCK_COUNT = count;
        }
        // Preinitialize a specified number of locks
        for (int i = 0; i  this.DEFAULT_LOCK_COUNT; i++) {
            this.lockMap.put(i, newReentrantLock(isFair)); }}public ReentrantLock get(T key) {
        return this.lockMap.get((key.hashCode()  1) % DEFAULT_LOCK_COUNT);
    }

    public void lock(T key) {
        ReentrantLock lock = this.get(key);
        lock.lock();
    }

    public void unlock(T key) {
        ReentrantLock lock = this.get(key); lock.unlock(); }}Copy the code

Since we learned about ReentrantLock in the previous article, the code above looks simple. The SegmentLock class constructor calls init to pre-initialize a specified number of locks, Then it provides the lock to obtain and unlock the method. One thing to note here is that we acquire the lock by unsigned shifting the hashCode value of the key one bit to the right, mod the number of locks, and then use this value as an index to retrieve the lock from the Map. The reason for the unsigned shift is that the hashCode value can be a negative number, which is still a negative number after the mod. Using a negative index to evaluate the Map will result in null, which will result in NPE when used later. Here's how to use it:

private static SegmentLockString segmentLock = new SegmentLock();

private static void consume(String userId, int amount) throws InterruptedException {
  System.out.println("verify that the account is normal...");
  TimeUnit.MILLISECONDS.sleep(500);
  ReentrantLock lock = segmentLock.get(userId);
  lock.lock();
  try {
    System.out.println("enter the deduction code block");
    Integer userAccountBalance = accountMap.get(userId);
    if (userAccountBalance = amount) {
      // deduction
      TimeUnit.MILLISECONDS.sleep(2000);
      userAccountBalance -= amount;
      accountMap.put(userId, userAccountBalance);
      System.out.println(Thread.currentThread().getName() + " deduction success");
    } else {
      TimeUnit.MILLISECONDS.sleep(1000);
      System.out.println(Thread.currentThread().getName() + " deduction failed, insufficient account balance."); }}finally{ lock.unlock(); }}Copy the code

As can be seen from the usage code, it is basically the same as the use of ReentrantLock, but because we have created a certain number of locks in advance, directly according to the user ID lock, then lock unlock operation, which can reduce the performance overhead of lock creation. This is a great performance boost for small numbers of concurrent paying users, which is why segmenting locking is used.

The problem here is that if the hash value of two user ids is exactly the same or the residual result of hash value is the same, then the two users will acquire the same lock. Then, when they consume gift certificates at the same time, one user first obtains the lock and performs the lock operation, and the other user also obtains the lock. In the execution of the lock will need to wait, because the last user has used the lock operation, wait until the last user successfully execute the lock release can be locked.

But you think about pay user base is, in itself, is not big, the problem of probability is very small, so the question is not big, here just remind may have such a situation, you will need to pay attention to the lock code block if the execution time is very long, there could be one of the users waiting time double, Of course, it is generally not recommended to lock a resource for a long time, i.e. the code block to be locked takes a long time to execute.

Advantages: For a small paying user base, performance is good when paying for locking because a portion of the lock is pre-created.

Disadvantages: Different users may obtain the same lock. As a result, the user needs to wait for the previous user to release the lock before applying the lock to proceed.

Hash lock

For segmenting lock, in fact, we set a scenario where the number of concurrent paying users is not very large. If our system has captured more loyal users and more and more users recognize our system with the gradual operation and iteration, then the number of concurrent paying users may have risen. In addition, a small number of users have begun to complain that our system has a lag when paying, and it takes a period of time.

This is probably what we implement piecewise lock number is no longer enough, a lot of user id hash value after taking more than the result is the same, then obtain the lock is the same, then you need to, such as serious when may be more than one user at the same time is of the same lock, the waiting time is longer.

This we will need to be optimized to improve, since lock is not enough that you need to create more lock, is that can be directly to create more number of lock, but paying user base has come up at this time, we may need a lock for each user distribution, this way I'm afraid not, it is we don't know which users need to use, Also, it's not realistic to create so many locks in the first place.

Then we can create a lock for each user who pays, and then remove it after the payment. Note that the lock is not pre-created, but created when the payment is completed. After creation, the lock will be put into the Map to prevent the same user from paying at the same time. Wait until the lock is released and then check the number of times the lock is added. If it is equal to the number of times the lock is removed from the Map, otherwise it will not be removed for the moment, just release the lock. Let's look at the code implementation:

public class HashLockT {
    private boolean fair = false;
    private final SegmentLockT segmentLock = new SegmentLock();
    private final ConcurrentHashMapT, ReentrantLockCount lockMap = new ConcurrentHashMap();

    public HashLock(a) {}public HashLock(boolean fair) {
        this.fair = fair;
    }

    public void lock(T key) {
        ReentrantLockCount lock;
        // Use segmenting locks to ensure thread safety when acquiring locks
        this.segmentLock.lock(key);
        try {
            lock = this.lockMap.get(key);
            if (lock == null) {
                lock = new ReentrantLockCount(this.fair);
                this.lockMap.put(key, lock);
            } else {
                If the lock exists in the map, the lock has been createdlock.incrementAndGet(); }}finally {
            this.segmentLock.unlock(key);
        }
        lock.lock();
    }

    public void unlock(T key) {
        ReentrantLockCount reentrantLockCount = this.lockMap.get(key);
        // If the number of times a lock is equal to one, the lock can be removed from the map
        if (reentrantLockCount.getCount() == 1) {
            this.segmentLock.lock(key);
            try {
                if (reentrantLockCount.getCount() == 1) {
                    this.lockMap.remove(key); }}finally {
                this.segmentLock.unlock(key);
            }
        }
        reentrantLockCount.unlock();
    }

    static class ReentrantLockCount {
        private ReentrantLock reentrantLock;
        // Record the number of locks
        private AtomicInteger count = new AtomicInteger(1);

        public ReentrantLockCount(boolean fair) {
            this.reentrantLock = new ReentrantLock(fair);
        }

        public void lock(a) {
            this.reentrantLock.lock();
        }

        public void unlock(a) {
            this.count.decrementAndGet();
            this.reentrantLock.unlock();
        }

        public int incrementAndGet(a) {
            return this.count.incrementAndGet();
        }

        public int getCount(a) {
            return this.count.get(); }}}Copy the code

ReentrantLockCount (ReentrantLockCount) is an inner class that wraps ReentrantLock and has a field that counts the number of times a lock has been created. It can be directly obtained from the Map. If the lock times are only once, it can be directly removed. The above lock and UNLOCK methods use segmental locking to ensure thread-safe lock acquisition and lock removal, which may lead to repeated lock creation and lock removal. As for the use of sectionalized locking:

private static HashLockString hashLock = new HashLock();
private static void consume1(String userId, int amount) {
  System.out.println("verify that the account is normal...");
  TimeUnit.MILLISECONDS.sleep(500);
  hashLock.lock(userId);
  try {
    System.out.println("enter the deduction code block");
    Integer userAccountBalance = accountMap.get(userId);
    if (userAccountBalance = amount) {
      // deduction
      TimeUnit.MILLISECONDS.sleep(2000);
      userAccountBalance -= amount;
      accountMap.put(userId, userAccountBalance);
      System.out.println(Thread.currentThread().getName() + " deduction success");
    } else {
      TimeUnit.MILLISECONDS.sleep(1000);
      System.out.println(Thread.currentThread().getName() + " deduction failed, insufficient account balance."); }}finally{ hashLock.unlock(userId); }}Copy the code

The use of code is actually similar to the use of segment lock, as for the inside of the sleep can be ignored, just to test when more can see the effect.

Advantages: The problem of locking shared by different users is solved

Disadvantages: Segmented locks are used to maintain lock acquisition and removal, as well as the number of locks added. The number of locks in segmented locks can be a performance bottleneck, and memory leaks may occur if the lock is not released successfully.

A weak reference to lock

As mentioned above in the disadvantages of HashLock, the number of segmented locks can be a performance bottleneck due to the need to maintain lock acquisition and removal through segmented locks. So is there a better solution? Using weak references, you can remove the fragment lock, recycle the lock object to the Java virtual machine, and then remove the lock that has been reclaimed. It can effectively avoid the problem of accidental memory leakage. Code implementation:

public class WeakHashLockT {

    /** * Lock number threshold in map */
    private static final int LOCK_SIZE_THRESHOLD = 1000;
    private ReferenceQueueReentrantLock queue = new ReferenceQueue();
    private ConcurrentHashMapT, WeakRefLockT, ReentrantLock lockMap = new ConcurrentHashMap();

    public ReentrantLock get(T key) {
        // You can set a threshold to remove some of the recovered locks when the number of locks exceeds this threshold
        if (this.lockMap.size()  LOCK_SIZE_THRESHOLD) {
            clearEmptyRef();
        }

        WeakRefLockT, ReentrantLock weakRefLock = this.lockMap.get(key);
        ReentrantLock lock = weakRefLock == null ? null : weakRefLock.get();
        while (lock == null) {
            lockMap.putIfAbsent(key, new WeakRefLock(new ReentrantLock(), this.queue, key));
          	// Get the lock from the Map again to ensure that the lock obtained by the same user is consistent
            weakRefLock = lockMap.get(key);
            lock = weakRefLock == null ? null : weakRefLock.get();
            if(lock ! =null) {
                return lock;
            }
            // If the heap resource is too tight, it may return empty. Some of the collected locks need to be removed
            clearEmptyRef();
        }
        return lock;
    }

    @SuppressWarnings("unchecked")
    public void clearEmptyRef(a) {
        Reference? extends ReentrantLock ref;
        while ((ref = this.queue.poll()) ! =null) {
            WeakRefLockT, ? extends ReentrantLock weakRefLock = (WeakRefLockT, ? extends ReentrantLock) ref;
            this.lockMap.remove(weakRefLock.key); }}static final class WeakRefLockT.K extends WeakReferenceK {

        private final T key;

        public WeakRefLock(K referent, ReferenceQueue? super K queue, T key) {
            super(referent, queue);
            this.key = key; }}}Copy the code

The above code mainly uses the feature of weak reference to remove the lock acquisition and the judgment process of the number of times to maintain lock creation. When the lock is acquired, it is directly obtained from the Map, and if it is empty, it is created. At the same time, it is necessary to explain the process of cleaning up the recovered lock in the code. First in the number of locks in the Map more than setting the threshold value of lock has been recovered after removing, mainly in order not to make too much in the Map have been lock occupancy resources, recycling of the removed mainly in case of resources is too tight, just created a weak reference lock was immediately recovered, then need to remove part of the lock has been recycled. Of course, if resources are really tight to this extent, you should also consider improving the configuration of the machine. Use code:

private static WeakHashLockString weakHashLock = new WeakHashLock();

private static void consume2(String userId, int amount) {
  System.out.println("verify that the account is normal...");
  TimeUnit.MILLISECONDS.sleep(500);
  ReentrantLock lock = weakHashLock.get(userId);
  lock.lock();
  try {
    System.out.println("enter the deduction code block");
    Integer userAccountBalance = accountMap.get(userId);
    if (userAccountBalance = amount) {
      // deduction
      TimeUnit.MILLISECONDS.sleep(2000);
      userAccountBalance -= amount;
      accountMap.put(userId, userAccountBalance);
      System.out.println(Thread.currentThread().getName() + " deduction success");
    } else {
      TimeUnit.MILLISECONDS.sleep(1000);
      System.out.println(Thread.currentThread().getName() + " deduction failed, insufficient account balance."); }}finally{ lock.unlock(); }}Copy the code

Use here will not say, are basically the same, the main difference is the implementation of the lock is different.

Advantages: The weak reference feature is used to remove the performance bottleneck caused by the fragment locking part of the code, and the collection operation is handed over to the Java VIRTUAL machine.

Cons: The code to get the lock seems a bit cumbersome, and there should be a more elegant way to do it.

Well, here is basically the implementation of fine-grained lock all said, each implementation of the advantages and disadvantages are also roughly summarized, according to the advantages and disadvantages of each implementation can also know the applicable scenario, when choosing according to the business needs to choose. There are more elegant implementations of this last one, which we'll discuss in the next article.

Reference: www.cnblogs.com/wxd0108/p/5...

Wechat public number: Rookiedev, Java background development, inspirational lifelong learning, adhere to the output of original dry goods, you can choose to pay attention to me now, or look at the historical article and then pay attention to it. Long press the QR code to pay attention, we work together to become better!

Search
About
mo4tech.com (Moment For Technology) is a global community with thousands techies from across the global hang out!Passionate technologists, be it gadget freaks, tech enthusiasts, coders, technopreneurs, or CIOs, you would find them all here.