What is the lock

In the multithreaded software world, the Data Race for shared resources is concurrency, and the most direct way to protect access to shared resource Data is to introduce locks.

POSIX Threads (Pthreads for short) is a common set of apis for parallel programming on multi-core platforms. Thread Synchronization is an important means of communication in parallel programming. One of the most typical uses is the lock mechanism provided by Pthreads to protect Critical sections shared by multiple threads (another common synchronization mechanism is barrier).

Lockless programming is another option, but it is beyond the scope of this article. Lock-free or lock optimization can be achieved by converting concurrent threads to single threads (Disruptor), functional programming, lock granularity control (ConcurrentHashMap buckets), and Semaphore.

Technically, locking can also be understood as serialization of a large number of concurrent requests, but please note that serialization cannot simply be equated to queueing, because queueing is no different from the real world, where everyone gets the resource fairly and on a first-come-first-served basis. However, in many cases, in order to consider the performance of multithreading or Unfair** to rob. Java ReentrantLock can be ReentrantLock, provides fair lock and unfair lock two implementations.

Note also that serial does not mean that there is only one queue, one at a time. Of course there can be multiple teams, multiple entry at a time. For example, in a restaurant with 10 tables, the waiter may let up to 10 people in at one time, and then let in the same number of people when someone comes out. Semaphore Semaphore in Java is equivalent to managing a batch of locks simultaneously.

The type of lock

Spin Lock

A spinlock is a non-blocking lock, which means that if a thread needs to acquire a spinlock, but the lock is already occupied by another thread, the thread will not be suspended, but will constantly spend CPU time trying to acquire the spinlock.

Mutex Lock

A mutex is a blocking lock. When a thread fails to acquire the mutex, the thread is suspended without consuming CPU time. When another thread releases the mutex, the operating system wakes up the suspended thread.

Reentrant Lock

A reentrant lock is a special mutex that can be acquired multiple times by the same thread without causing a deadlock.

Lock for example

The local lock

In Java, local locking can be implemented using synchronized and lock.


//synchronized

    public synchronized void demoMethod(a){}
    
    public void demoMethod(a){
        synchronized (this)
        {
            //other thread safe code}}private final Object lock = new Object();
    public void demoMethod(a){
        synchronized (lock)
        {
            //other thread safe code}}public synchronized static void demoMethod(a){}

//lock

   private final Lock queueLock = new ReentrantLock();
 
   public void printJob(Object document)
   {
      queueLock.lock();
      try
      {
         Long duration = (long) (Math.random() * 10000);
         System.out.println(Thread.currentThread().getName() + ": PrintQueue: Printing a Job during " + (duration / 1000) + " seconds :: Time - " + new Date());
         Thread.sleep(duration);
      } catch (InterruptedException e)
      {
         e.printStackTrace();
      } finally
      {
         System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName()); queueLock.unlock(); }}Copy the code

A lock is not static; it locks an instance of an object. Lock static is the type of an object that is locked.

Some of the features

  • Reentrant. You can enter the testWrite method directly without reapplying the lock. Synchronized and lock are reentrant locks.
    synchronized void testRead(a){
        this.testWrite();
    }
    synchronized void testWrite(a){}
Copy the code
  • Interruptible locking. For example, if A is executing the code in the lock and another thread B is waiting to acquire the lock, the lock is interruptible if B can interrupt. Synchronized is not a breakable Lock, whereas Lock is a breakable Lock.
  • Fair locks and unfair locks. Acquiring locks in the order in which they were requested is fair locking. Synchronized is a non-fair lock. Lock Is a non-fair lock by default, but can be set to a fair lock.

contrast

The name of the advantages disadvantages
synchronized Simple implementation, clear semantics, easy to JVM stack trace, lock unlock process is automatically controlled by the JVM, provides a variety of optimization schemes, more widely used Pessimistic exclusive lock that cannot perform advanced functions
lock Timed, pollable, and interruptible lock acquisition operations provide read-write, fair, and unfair locks Unlock needs to be released manually and is not suitable for stack tracing by the JVM

A distributed lock

The purpose of using distributed locks is two-fold. One is to avoid multiple idempotent operations and improve efficiency. One is to avoid data inconsistencies caused by multiple nodes performing non-idempotent operations simultaneously. Now let’s look at how to implement distributed locks. In the Java environment, there are three ways to implement distributed locks: through databases, through Redis, and through Zk.

Through the database

Using exceptions to implement distributed locking with primary keys and other constraints is beyond the scope of this article. The following is a distributed lock based on database exclusive lock

/** * Timeout to obtain lock *@param lockID
     * @param timeOuts
     * @return
     * @throws InterruptedException
     */
    public boolean acquireByUpdate(String lockID, long timeOuts) throws InterruptedException, SQLException {

        String sql = "SELECT id from test_lock where id = ? for UPDATE ";
        long futureTime = System.currentTimeMillis() + timeOuts;
        long ranmain = timeOuts;
        long timerange = 500;
        connection.setAutoCommit(false);
        while (true) {
            CountDownLatch latch = new CountDownLatch(1);
            try {
                PreparedStatement statement = connection.prepareStatement(sql);
                statement.setString(1, lockID);
                statement.setInt(2.1);
                statement.setLong(1, System.currentTimeMillis());
                boolean ifsucess = statement.execute();// If it succeeds, the lock is obtained
                if (ifsucess)
                    return true;
            } catch (SQLException e) {
                e.printStackTrace();
            }
            latch.await(timerange, TimeUnit.MILLISECONDS);
            ranmain = futureTime - System.currentTimeMillis();
            if (ranmain <= 0)
                break;
            if (ranmain < timerange) {
                timerange = ranmain;
            }
            continue;
        }
        return false;

    }


    /** * release lock *@param lockID
     * @return
     * @throws SQLException
     */
    public void unlockforUpdtate(String lockID) throws SQLException {
        connection.commit();

    }
Copy the code

Through the cache system

lock

public class RedisTool {
 
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
 
    /** * Attempted to obtain a distributed lock *@paramJedis Redis client *@paramLockKey lock *@paramRequestId requestId *@paramExpireTime Expiration time *@returnCheck whether the file is successfully obtained */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
 
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
 
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false; }}Copy the code

The first one is key, and we use key as the lock because key is unique. The second value is value, we pass requestId, many children may not understand, there is a key as a lock is not enough, why use value? The reason is that when we talked about reliability above, the distributed lock must meet the fourth condition to unlock the bell. By assigning value to requestId, we can know which request added the lock, which can be based on when unlocking. The requestId can be generated using the uuID.randomuuid ().toString() method. The third parameter is NXXX. We fill in NX for this parameter, which means SET IF NOT EXIST, that is, when key does NOT EXIST, we perform SET operation; If the key already exists, no operation is performed. Expx = PX; expx = PX; expx = PX; The fifth parameter is time, which corresponds to the fourth parameter and represents the expiration time of the key.

unlock

public class RedisTool {
 
    private static final Long RELEASE_SUCCESS = 1L;
 
    /** * Release the distributed lock *@paramJedis Redis client *@paramLockKey lock *@paramRequestId requestId *@returnCheck whether the release is successful */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
 
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
 
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false; }}Copy the code

In the first line, we write a simple Lua script. In the second line, we pass the Lua code to jedis.eval() and assign KEYS[1] to lockKey and ARGV[1] to requestId. The eval() method hands Lua code to the Redis server for execution.

Redlock based on the implementation of distributed lock debate see

Redlock

how-to-do-distributed-locking

Implemented through ZK

Use curator to implement distributed locking.

public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
    try {
        return interProcessMutex.acquire(timeout, unit);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return true;
}
public boolean unlock(a) {
    try {
        interProcessMutex.release();
    } catch (Throwable e) {
        log.error(e.getMessage(), e);
    } finally {
        executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS);
    }
    return true;
}
Copy the code

Distributed lock comparison

way advantages disadvantages
Based on the DB Use the database directly, easy to understand There will be all kinds of problems, and the solution will become more and more complicated in the process

There is some overhead involved in operating the database, and performance concerns need to be considered

Using row-level locking for a database is not always a good idea, especially if our lock table is not large
Based on the cache Good performance, easy to implement It is not very reasonable to control lock failure time by timeout
Based on the ZK Effectively solve single point problems, non-reentrant problems, non-blocking problems and locks that cannot be released. It’s relatively simple to implement Performance is not as good as using caching for distributed locking. Some understanding of how ZK works is required

conclusion

Zookeeper is much more reliable than Redis but less efficient. If the concurrency is not very large, zooKeeper is preferred for reliability. For efficiency, the redis implementation is preferred.