Concurrent programming - ReentrantLock

If you are interested in learning more about it, please visit my personal website: Yetong Space

One: Basic introduction

Starting from Java 5, an advanced concurrency handling java.util.concurrentpackage has been introduced, which provides a large number of more advanced concurrency functions, which can greatly simplify the writing of multi-threaded programs. We know that the Java language directly provides the synchronized keyword for locking, but this kind of lock is very heavy on the one hand, and on the other hand, it must wait all the time when acquiring it, and there is no additional attempt mechanism. java.util.concurrent.locksThe ReentrantLock provided by the package is used to replace the synchronized lock .

Lock is a very important interface in Java concurrent programming. It can translate the concept of "lock" more literally than the synchronized keyword. Lock needs to be manually locked and unlocked. Generally, it is locked through the lock.lock() method. lock.unlock() method to unlock. Generally, unlock( ) is written in the finally block to prevent deadlock. And ReentrantLock implements the Lock interface.

ReentrantLock is a reentrant mutex, also known as "exclusive lock". Reentrant means that the ReentrantLock lock can be acquired multiple times by the same thread without deadlock. ReentraantLock manages all threads that acquire the lock through a FIFO waiting queue. Under the "fair lock" mechanism, threads queue up to acquire locks in turn; while "unfair locks" acquire locks regardless of whether they are at the beginning of the queue when the lock is available.

ReentrantLock and synchronized comparison

  • Synchronized is a grammar provided by the Java language level, and ReentrantLock is a lock implemented by Java code.
  • Synchronized is an exclusive lock, the process of locking and unlocking is automatic, easy to operate, but not flexible enough. ReentrantLock is also an exclusive lock. The process of locking and unlocking needs to be done manually. It is not easy to operate, but it is very flexible.
  • Synchronized cannot respond to interrupts, and a thread waits until it can acquire a lock; ReentrantLock can respond to interrupts.

Basic usage example:

public class Counter {
    
    
    private final Lock lock = new ReentrantLock();
    private int count;

    public void add(int n) {
    
    
        lock.lock();
        try {
    
    
            count += n;
        } finally {
    
    
            lock.unlock();
        }
    }
}

Two: reentrant

Reentrancy means that if the same thread acquires the lock for the first time, it has the right to acquire the lock again because it is the owner of the lock. If it is a non-reentrant lock, then when you get the lock for the second time, you will also be blocked by the lock

The sample code is as follows:

public class Demo {
    
    
  static Lock lock = new ReentrantLock();

  public static void main(String[] args) {
    
    
    method1();
  }

  public static void method1() {
    
    
    lock.lock();
    try {
    
    
      System.out.println("execute method1");
      method2();
    } finally {
    
    
      lock.unlock();
    }
  }

  public static void method2() {
    
    
    lock.lock();
    try {
    
    
      System.out.println("execute method2");
    } finally {
    
    
      lock.unlock();
    }
  }
}

insert image description here

Three: Can be interrupted

public class Demo {
    
    
  static Lock lock = new ReentrantLock();

  public static void main(String[] args) {
    
    
    Thread t1 = new Thread(() -> {
    
    
      try {
    
    
        // 如果没有竞争,那么此方法就会获取lock对象锁
        // 如果有竞争,就会进入阻塞队列,可以被其他线程用interrupt方法打断
        System.out.println("尝试获得锁");
        lock.lockInterruptibly();
      } catch (InterruptedException e) {
    
    
        e.printStackTrace();
        // 没有获得锁,在等待时被打断了
        return;
      }
      try {
    
    
        System.out.println("获取到锁了");
      } finally {
    
    
        lock.unlock();
      }
    }, "t1");

    lock.lock();
    t1.start();

    try {
    
    
      Thread.sleep(1);
      System.out.println("打断t1");
      t1.interrupt();
    } catch (InterruptedException e) {
    
    
      e.printStackTrace();
    }
  }
}

insert image description here

Four: lock timeout

When no parameters are passed in the tryLock method, the default is to try to acquire the lock immediately:

public class Demo {
    
    
  static Lock lock = new ReentrantLock();

  public static void main(String[] args) {
    
    
    Thread t1 = new Thread(() -> {
    
    
      System.out.println("t1线程启动。。。。。。");
      if (!lock.tryLock()) {
    
    
        System.out.println("t1线程获取锁失败,返回");
        return;
      }
      try {
    
    
        System.out.println("t1线程获得了锁");
      } finally {
    
    
        lock.unlock();
      }
    }, "t1");

    lock.lock();
    System.out.println("主线程获得了锁");
    t1.start();
    try {
    
    
      Thread.sleep(1);
    } catch (InterruptedException e) {
    
    
      e.printStackTrace();
    } finally {
    
    
      System.out.println("主线程释放锁");
      lock.unlock();
    }
  }
}

insert image description here

But tryLock also has an overloaded method tryLock(long time, TimeUnit unit), which will keep trying to acquire the lock during the waiting time:

public class Demo {
    
    
  static Lock lock = new ReentrantLock();

  public static void main(String[] args) {
    
    
    Thread t1 = new Thread(() -> {
    
    
      System.out.println("t1线程启动。。。。。。");
      try {
    
    
        if (!lock.tryLock(2, TimeUnit.SECONDS)) {
    
    
          System.out.println("t1线程获取锁失败,返回");
          return;
        }
      } catch (InterruptedException e) {
    
    
        System.out.println("等待被打断");
        e.printStackTrace();
        return;
      }
      try {
    
    
        System.out.println("t1线程获得了锁");
      } finally {
    
    
        lock.unlock();
      }
    }, "t1");

    lock.lock();
    System.out.println("主线程获得了锁");
    t1.start();
    try {
    
    
      Thread.sleep(1);
    } catch (InterruptedException e) {
    
    
      e.printStackTrace();
    } finally {
    
    
      System.out.println("主线程释放锁");
      lock.unlock();
    }
  }
}

insert image description here

Five: fair lock

Fair locks and unfair locks:

  • The realization of fair lock is that whoever waits the longest will acquire the lock first
  • Unfair lock is a process of random acquisition. Whoever is lucky, which thread is polled by the CPU time slice, can acquire the lock

ReentrantLock is unfair by default, but you can set whether it is fair or not according to your needs. The source code of the ReentrantLock construction method is as follows:

    /**
     * 创建一个ReentrantLock实例
     * 该方法等同于调用ReentrantLock(false)
     */
    public ReentrantLock() {
    
    
        sync = new NonfairSync();
    }

    /**
     * 根据传入的公平策略创建ReentrantLock实例
     * @param fair true为公平策略,false为非公平策略
     */
    public ReentrantLock(boolean fair) {
    
    
        sync = fair ? new FairSync() : new NonfairSync();
    }

Compared with unfair locks, ReentrantLock fair locks have lower system throughput under multi-threaded concurrency because they need to wait in line. Therefore, the ReentrantLock fair lock is suitable for application scenarios where multi-threaded concurrency is not very high and tends to come first.

Six: Condition variables

The condition variable function in ReentrantLock is similar to ordinary synchronized wait and notify. We can use ReentrantLlock locks to cooperate with the await() and signal() or signalAll() methods on the Condition object to achieve inter-thread collaboration. The difference from synchronized wait and notify is that there can be multiple condition variables in ReentrantLock, enabling finer control threads.

Before introducing the use of the method, let's first understand what a Condition is. Conditions can be thought of as a replacement for Object monitors. As we all know, Object has wait() and notify() methods for communication between threads. And these two methods can only be called within the synchronized synchronization block, all threads waiting and waking up need to be associated with the WaitSet collection of the monitor object. Condition can also implement the above thread communication. The difference is that there is only one monitor object associated with the synchronized lock object, so there is only one waiting queue. A ReentrantLock can have multiple conditions, so that multiple waiting queues can be used on the basis of using the same lock object according to different business needs, so that threads of different natures can be added to different waiting queues.

The implementation class of Condition in AQS is ConditionObject, which is an internal class of AQS, so it cannot be instantiated directly. Can be used with ReentrantLock. There is a newCondition() method in ReentrantLock to instantiate a ConditionObject object, so you can call the newCondition() method multiple times to get multiple waiting queues.

manual:

  • A lock needs to be acquired before await
  • After await is executed, the lock will be released and enter conditionObject to wait
  • After the thread of await is awakened (or interrupted, or timed out) to re-compete for the lock and successfully compete for the lock, it will continue to execute after await

The sample code is as follows:

@Slf4j
public class Demo {
    
    

  private static ReentrantLock lock = new ReentrantLock();
  // 等烟休息室
  static Condition cigaretteRoom = lock.newCondition();
  // 等外卖休息室
  static Condition eattingRoom = lock.newCondition();

  static boolean hasCigarette = false;
  static boolean hasTakeout = false;


  public static void main(String[] args) throws InterruptedException {
    
    
    // 小南
    new Thread(() -> {
    
    
      lock.lock();
      try {
    
    
        log.debug("[{}]", hasCigarette);
        while (!hasCigarette) {
    
    
          log.debug("没烟,先歇会!");
          try {
    
    
            cigaretteRoom.await();
          } catch (InterruptedException e) {
    
    
            e.printStackTrace();
          }
        }
        log.debug("有烟没?[{}]", hasCigarette);
        if (hasCigarette) {
    
    
          log.debug("可以开始干活了");
        }
      } finally {
    
    
        lock.unlock();
      }
    }, "小南").start();

    // 小女等外卖
    new Thread(() -> {
    
    
      lock.lock();
      try {
    
    
        log.debug("外卖送到没?[{}]", hasTakeout);
        while (!hasTakeout) {
    
    
          log.debug("没外卖,先歇会!");
          try {
    
    
            eattingRoom.await();
          } catch (InterruptedException e) {
    
    
            e.printStackTrace();
          }
        }
        log.debug("外卖送到没?[{}]", hasTakeout);
        if (hasTakeout) {
    
    
          log.debug("可以开始干活了");
        } else {
    
    
          log.debug("没干成活...");
        }
      } finally {
    
    
        lock.unlock();
      }
    }, "小女").start();

    // 送烟的来了
    Thread.sleep(1000);
    new Thread(() -> {
    
    
      lock.lock();
      try {
    
    
        hasCigarette = true;
        cigaretteRoom.signal();
      } finally {
    
    
        lock.unlock();
      }
    }, "送烟的").start();

    // 送外卖的来了
    Thread.sleep(1000);
    new Thread(() -> {
    
    
      lock.lock();
      try {
    
    
        hasTakeout = true;
        eattingRoom.signal();
      } finally {
    
    
        lock.unlock();
      }
    }, "送外卖的").start();
  }
}

Guess you like

Origin blog.csdn.net/tongkongyu/article/details/129340152
Recommended