The correct way to acquire locks by key in Java

1. Overview

In this article we'll see how to acquire a lock on a specific key so that operations on that key are thread-safe and don't interfere with other keys.
In general, we need to implement two methods:

void lock(String key)
void unlock(String key)

This article takes strings as keys as an example. You can transform them into any type of keys according to actual needs, rewrite the equas and hashCode methods, and ensure uniqueness.

2. Simple mutex

Assuming that the current thread needs to acquire the lock, specific code needs to be executed, otherwise this scenario will not be executed.
We can maintain a series of Sets of Keys, add them to the Set when in use, and remove the corresponding Keys when unlocking.
At this point, thread safety issues need to be considered. Therefore, it is necessary to use a thread-safe Set implementation, such as a thread-safe Set based on ConcurrentHashMap.

public class SimpleExclusiveLockByKey {
    
    

    private static Set<String> usedKeys= ConcurrentHashMap.newKeySet();
    
    public boolean tryLock(String key) {
    
    
        return usedKeys.add(key);
    }
    
    public void unlock(String key) {
    
    
        usedKeys.remove(key);
    }

}

Use Cases:

String key = "key";
SimpleExclusiveLockByKey lockByKey = new SimpleExclusiveLockByKey();
try {
    
    
    lockByKey.tryLock(key);
    // 在这里添加对该 key 获取锁之后要执行的代码
} finally {
    
     // 非常关键
    lockByKey.unlock(key);
}
    

Note that it must be unlocked in the finally code block to ensure that it can be unlocked normally even if an exception occurs.

3. Press the key to acquire and release the lock

The above code can be guaranteed to be executed after the lock is acquired, but it cannot achieve the effect of waiting for threads that have not acquired the lock.
Sometimes, we need to make threads that have not acquired the corresponding lock wait.
The process is as follows:

  • The first thread acquires a lock on a key
  • The second thread acquires the lock of the same key, the second thread needs to wait
  • The first thread releases the lock on a key
  • The second thread acquires the lock on the key and executes its code

3.1 Define Lock using thread counter

We can use ReentrantLock to implement thread blocking.
We encapsulate Lock through an inner class. This class counts the number of threads executing on a key. Two methods are exposed, one is to increase the number of threads, and the other is to reduce the number of threads.

private static class LockWrapper {
    
    
    private final Lock lock = new ReentrantLock();
    private final AtomicInteger numberOfThreadsInQueue = new AtomicInteger(1);

    private LockWrapper addThreadInQueue() {
    
    
        numberOfThreadsInQueue.incrementAndGet(); 
        return this;
    }

    private int removeThreadFromQueue() {
    
    
        return numberOfThreadsInQueue.decrementAndGet(); 
    }

}

3.2 Handling queued threads

Next continue to use ConcurrentHashMap with key as key and LockWrapper as value.
Ensure that the same key uses the same lock in the same LockWrapper.

private static ConcurrentHashMap<String, LockWrapper> locks = new ConcurrentHashMap<String, LockWrapper>();

When a thread wants to acquire the lock of a key, it needs to check whether the LockWrapper corresponding to the key already exists.

If it does not exist, create a LockWrapper and set the counter to 1.
If it exists, add 1 to the corresponding LockWrapper

public void lock(String key) {
    
    
    LockWrapper lockWrapper = locks.compute(key, (k, v) -> v == null ? new LockWrapper() : v.addThreadInQueue());
    lockWrapper.lock.lock();
}

3.3 Unlock and remove Entry

Decrement the waiting queue by one when unlocking.
When the number of threads corresponding to the current key is 0, it can be removed from ConcurrentHashMap.

public void unlock(String key) {
    
    
    LockWrapper lockWrapper = locks.get(key);
    lockWrapper.lock.unlock();
    if (lockWrapper.removeThreadFromQueue() == 0) {
    
     
        // NB : We pass in the specific value to remove to handle the case where another thread would queue right before the removal
        locks.remove(key, lockWrapper);
    }
}

3.4 Summary

The final effect is as follows:

public class LockByKey {
    
    
    
    private static class LockWrapper {
    
    
        private final Lock lock = new ReentrantLock();
        private final AtomicInteger numberOfThreadsInQueue = new AtomicInteger(1);
        
        private LockWrapper addThreadInQueue() {
    
    
            numberOfThreadsInQueue.incrementAndGet(); 
            return this;
        }
        
        private int removeThreadFromQueue() {
    
    
            return numberOfThreadsInQueue.decrementAndGet(); 
        }
        
    }
    
    private static ConcurrentHashMap<String, LockWrapper> locks = new ConcurrentHashMap<String, LockWrapper>();
    
    public void lock(String key) {
    
    
        LockWrapper lockWrapper = locks.compute(key, (k, v) -> v == null ? new LockWrapper() : v.addThreadInQueue());
        lockWrapper.lock.lock();
    }
    
    public void unlock(String key) {
    
    
        LockWrapper lockWrapper = locks.get(key);
        lockWrapper.lock.unlock();
        if (lockWrapper.removeThreadFromQueue() == 0) {
    
     
            // NB : We pass in the specific value to remove to handle the case where another thread would queue right before the removal
            locks.remove(key, lockWrapper);
        }
    }
    
}

Example usage:

String key = "key"; 
LockByKey lockByKey = new LockByKey(); 
try {
    
     
    lockByKey.lock(key);
    // insert your code here 
} finally {
    
     // CRUCIAL 
    lockByKey.unlock(key); 
}

4. Allow the same key to run in multiple threads at the same time

We also need to consider another scenario: Only one thread is allowed to execute for the same key at the same time. What if we want to realize that for the same key, what should we do to allow n threads to run at the same time?
For easy understanding, we assume that the same key allows two threads.

The first thread wants to acquire the lock of a key, and
the second thread also wants to acquire the lock of the key, and the
third thread also wants to acquire the lock of the key. The thread needs to wait for the first or second The Semaphore can only be executed after a thread releases the lock, which
is very suitable for this scenario. Semaphore can control the number of threads running at the same time.

public class SimultaneousEntriesLockByKey {
    
    

    private static final int ALLOWED_THREADS = 2;
    
    private static ConcurrentHashMap<String, Semaphore> semaphores = new ConcurrentHashMap<String, Semaphore>();
    
    public void lock(String key) {
    
    
        Semaphore semaphore = semaphores.compute(key, (k, v) -> v == null ? new Semaphore(ALLOWED_THREADS) : v);
        semaphore.acquireUninterruptibly();
    }
    
    public void unlock(String key) {
    
    
        Semaphore semaphore = semaphores.get(key);
        semaphore.release();
        if (semaphore.availablePermits() == ALLOWED_THREADS) {
    
     
            semaphores.remove(key, semaphore);
        }  
    }
    
}

Use Cases:

String key = "key"; 
SimultaneousEntriesLockByKey lockByKey = new SimultaneousEntriesLockByKey(); 
try {
    
     
    lockByKey.lock(key); 
    // 在这里添加对该 key 获取锁之后要执行的代码
} finally {
    
     // 非常关键
    lockByKey.unlock(key); 
}

V. Conclusion

This article demonstrates how to lock a key to ensure that the concurrent operations on the key are limited, and one or more threads can execute the same key at the same time.
Relevant code: https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-concurrency-advanced-4

Original link: https://blog.csdn.net/w605283073/article/details/127858281

Guess you like

Origin blog.csdn.net/weixin_49934658/article/details/130523565