Java concurrent programming - bank deposit and withdrawal through ReentrantLock, Condition

      The java.util.concurrent.locks package provides a framework interface and classes for locks and wait conditions, which differ from the built-in synchronization and monitors. The framework allows for more flexible use of locks and conditions, but at the cost of a more difficult syntax. 

        The Lock interface supports locking rules with different semantics (reentrancy, fairness, etc.) that can be used in the context of non-blocking constructs (including hand-over-hand and lock reordering algorithms). The main implementation is ReentrantLock. 

        The ReadWriteLock interface similarly defines some locks that can be shared by readers and exclusive by writers. This package provides only one implementation, ReentrantReadWriteLock, because it works in most standard usage contexts. But programmers can create their own implementations for non-standard requirements. 

   The following is the relevant class diagram of the locks package:

 

        In the past, we used the synchronized keyword when synchronizing a piece of code or an object, using the built-in features of the Java language. However, the synchronized feature also caused problems in many scenarios, such as:

        On a synchronous resource, first thread A acquires the lock of the resource and starts to execute. At this time, other threads that want to operate the resource must wait. If thread A is in a state of long-term operation for some reason, such as waiting for the network, retrying repeatedly, etc. Then other threads have no way to process their tasks in a timely manner, and can only wait indefinitely. If thread A's lock can be automatically released after being held for a period of time, can't other threads use the resource? Then there is the shared lock and exclusive lock similar to the database, can it also be applied to the application? Therefore, the introduction of the Lock mechanism can solve these problems very well.

  Lock provides more functionality than synchronized. But pay attention to the following points:

    Lock is not a built-in Java language, synchronized is a Java language keyword, so it is a built-in feature. Lock is a class through which synchronous access can be achieved;

    There is a very big difference between Lock and synchronized. Using synchronized does not require the user to manually release the lock. When the synchronized method or synchronized code block is executed, the system will automatically let the thread release the occupied lock; while Lock must require the user to release the lock. Manually release the lock. If the lock is not actively released, it may cause a deadlock phenomenon.

 

1. ReentrantLock

  When we think of locks, we generally think of synchronized locks, that is, Synchronized. The reentrant lock introduced here is more efficient. IBM has an introduction to reentrant locks: A more flexible and scalable locking mechanism in JDK 5.0

  Here is a brief introduction to the classification of reentrant locks: (Assume that thread A acquires the lock, and now A has completed its execution, releasing the lock and awakening thread B waiting to be awakened. However, A performs the wake-up operation until B actually acquires the lock There may be a thread C that has acquired the lock during the time, causing B, who is queuing, to be unable to acquire the lock)

  1) Fair lock: 

     Since B is waiting to be awakened first, in order to ensure the principle of fairness, the fair lock will let B obtain the lock first.

  2) Unfair lock

     B is not guaranteed to acquire the lock object first.

  These two kinds of locks only need to be distinguished when constructing the ReentrantLock object. When the parameter is set to true, it is a fair lock, and when it is false, it is an unfair lock. At the same time, the default constructor also creates an unfair lock.

    private Lock lock = new ReentrantLock(true); 

The fair lock of ReentrantLock has made great sacrifices in performance and effectiveness. You can refer to the description in the article posted by IBM.

2. Condition variable Condition

  Condition is an interface under the java.util.concurrent.locks package. The Condition interface describes condition variables that may be associated with locks. These variables are similar in usage to the implicit monitors accessed using Object.wait, but provide more powerful functionality. It is important to point out that a single Lock may be associated with multiple Condition objects. To avoid compatibility issues, the names of the Condition methods are different from those in the corresponding Object version. 

       Condition decomposes the Object monitor methods (wait, notify, and notifyAll) into distinct objects to provide multiple wait-sets per object by combining these objects with any Lock implementation. Among them, Lock replaces the use of synchronized methods and statements, and Condition replaces the use of Object monitor methods. 

   A Condition (also known as a condition queue or condition variable) provides a means for a thread to remain suspended (i.e. "wait") under a certain state condition until notified by another thread. Because access to this shared state information occurs in different threads, it must be protected, so some form of lock is associated with the Condition.

        The Condition instance is essentially bound to a lock.

  The source code under the Locks package is no longer analyzed here.

 

3. ReentrantLock and Condition design multi-threaded deposits and withdrawals

1. When depositing, there cannot be a thread withdrawing money. When withdrawing money, there must be no thread depositing.

2. When withdrawing money, the withdrawal operation can only be performed if the balance is greater than the withdrawal amount, otherwise it will be prompted that the balance is insufficient.

3. When withdrawing money, if the amount is insufficient, block the current thread and wait for 2s (there may be other threads depositing money).

    If no other thread completes the deposit within 2s, or the amount is still insufficient, the print amount is insufficient.

    If the other deposit is sufficient, the blocking thread will be notified and the withdrawal operation will be completed.

/**
 * Ordinary bank account, no overdraft
 */ 
public  class MyCount {
     private String oid; // account 
    private  int cash;    // account balance
     // account lock 
    private Lock lock = new ReentrantLock( true );
     private Condition _save = lock.newCondition(); // deposit condition 
    private Condition _draw = lock.newCondition(); // withdrawal condition 

    MyCount(String oid, int cash) {
         this .oid = oid;
         this .cash =cash;
    }

    /**
     * deposit
     * @param x operation amount
     * @param name 操作人
     */
    public void saving(int x, String name) {
        lock.lock(); // Get the lock 
        if (x > 0 ) {
            cash += x; // Deposit 
            System.out.println(name + "deposit" + x + ", the current balance is " + cash);
        }
        _draw.signalAll(); // Wake up all waiting threads. 
        lock.unlock(); // release the lock 
    }

    /**
     * Withdrawal
     * @param x operation amount
     * @param name operator
      */ 
    public  void drawing( int x, String name) {
        lock.lock(); // Acquire the lock 
        try {
             if (cash - x < 0 ) {
                System.out.println(name + "blocking" );
                _draw.await( 2000,TimeUnit.MILLISECONDS); // Block the withdrawal operation, the lock will be automatically released after await, until it is woken up and automatically acquired 
            }
             if (cash-x>=0 ){
                cash -= x; // Withdrawal 
                System.out.println(name + "withdrawal" + x + ", the current balance is " + cash);
            }else{
                System.out.println(name +" insufficient balance, current balance is "+cash+" withdrawal amount is "+ x);
            }
            // Wake up all deposit operations, there is no actual effect here, because there is no blocking operation in the deposit code 
            _save.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace ();
        } finally {
            lock.unlock(); // release the lock 
        }
    }
}

The reentrant lock here can also be set as an unfair lock, so that blocking the withdrawal thread may be followed by other deposit and withdrawal operations.

 /**
     * Deposit thread class
     */
    static class SaveThread extends Thread {
        private String name; // 操作人
        private MyCount myCount; // 账户
        private int x; // 存款金额

        SaveThread(String name, MyCount myCount, int x) {
            this.name = name;
            this.myCount = myCount;
            this.x = x;
        }

        public void run() {
            myCount.saving(x, name);
        }
    }

    /**
     * Withdrawal thread class
     */
    static class DrawThread extends Thread {
        private String name; // 操作人
        private MyCount myCount; // 账户
        private int x; // 存款金额

        DrawThread(String name, MyCount myCount, int x) {
            this.name = name;
            this.myCount = myCount;
            this.x = x;
        }

        public void run() {
            myCount.drawing(x, name);
        }
    }

    public  static  void main(String[] args) {
         // Create a concurrent access account 
        MyCount myCount = new MyCount("95599200901215522", 1000 );
         // Create a thread pool 
        ExecutorService pool = Executors.newFixedThreadPool(3 );
        Thread t1 = new SaveThread("张三", myCount, 100);
        Thread t2 = new SaveThread("李四", myCount, 1000);
        Thread t3 = new DrawThread("王五", myCount, 12600);
        Thread t4 = new SaveThread("老张", myCount, 600);
        Thread t5 = new DrawThread("老牛", myCount, 2300);
        Thread t6 = new DrawThread("胖子", myCount, 1800);
        Thread t7 = new SaveThread("Test", myCount, 200 );
         // Execute each thread 
        pool.execute(t1);
        pool.execute(t2);
        pool.execute(t3);
        pool.execute(t4);
        pool.execute(t5);
        pool.execute(t6);
        pool.execute(t7);

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace ();
        }
        // Close the thread pool 
        pool.shutdown();
    }
}

Multiple deposit and withdrawal threads are defined in the above class, and the execution results are as follows:

S1 deposit 100, current balance is 1100
S3 deposit 600, current balance is 1700
D2 block
S2 deposit 1000, current balance is 2700
D2 withdraw 2300, current balance is 400
D3 block
S4 deposit 200, current balance is 600
D3 Insufficient balance, The current balance is 600 The withdrawal amount is 1800
D1 is blocked The
D1 balance is insufficient, the current balance is 600 The withdrawal amount is 12600

The execution steps are as follows:

  1. Initialize the account with a balance of 100.
  2. S1, S3 complete the deposit.
  3. D2 withdraws money, the balance is insufficient, releases the lock and blocks the thread, and enters the waiting queue.
  4.  After S2 completes the deposit operation, it will wake up the suspended thread, and then D2 completes the withdrawal.
  5.  D3 withdraws money, the balance is insufficient, releases the lock and blocks the thread, and enters the waiting queue.
  6.  After S4 completes the deposit operation, it wakes up D3, but the balance is still insufficient, and the D3 withdrawal fails.
  7.  D1 makes a withdrawal, waits for 2s, no thread wakes it up, and the withdrawal fails.

It should be noted here that when Condition calls the await() method, the current thread will release the lock (otherwise it will be no different from Sychnize)

When changing the lock in the bank account to an unfair lock, the execution result is as follows:

1 deposit 100, current balance is 1100
S3 deposit 600, current balance is 1700
D2 is blocked
S2 deposit 1000, current balance is 2700
D3 withdraws 1800, the current balance is 900
The balance of D2 is insufficient, the current balance is 900, and the withdrawal amount is 2300
S4 deposit 200, current balance is 1100
D1 is blocked
The balance of D1 is insufficient, the current balance is 1100, and the withdrawal amount is 12600

When the balance of D2 withdrawal is insufficient, the lock is released and the waiting state is entered. However, when the S2 thread completes the deposit, the D2 thread is not executed immediately, but is queued by D3.

The difference between fair locks and unfair locks can be seen from the execution results. Fair locks can ensure that waiting threads are executed first, but unfair locks may be queued by other threads.

 

Fourth, the application of ReentrantLock and Condition in ArrayBlockingQueue

A very typical application of reentrant locks in the JDK source code is BlockingQueue, which can be known from the member variables in its source code (ArrayBlockingQueue as an example):

/** The queued items */
    final Object[] items;

    /** items index for next take, poll, peek or remove */
    int takeIndex;

    /** items index for next put, offer, or add */
    int putIndex;

    /** Number of elements in the queue */
    int count;

    /*
     * Concurrency control uses the classic two-condition algorithm
     * found in any textbook.
     */

    /** Main lock guarding all access */
    // Mainly solve the thread safety problem of multi-threaded access
    final ReentrantLock lock;

    /** Condition for waiting takes */
    // When adding elements, wake up the consuming thread through notEmpty (waiting for this condition)
    private final Condition notEmpty;

    /** Condition for waiting puts */
    // When deleting an element, wake up the spawning thread via notFull (waiting for this condition)
    private final Condition notFull;

ArrayBlockingQueue is a typical producer-consumer model that stores elements through an array. To ensure thread safety of adding and removing elements, reentrant locks and condition variables have been added.

Reentrant locks mainly ensure that multi-threaded operations on blocking queues are thread-safe. At the same time, in order to allow blocked consumers or producers to be automatically awakened, condition variables are introduced here.

When the queue is full, the Producer will be blocked. At this time, if the Customer consumes an element, the blocked Producer will be automatically woken up and add elements to the queue.

 

The above two examples show the flexibility and practicability of ReentrantLock and Condition under the java.util.concurrent.locks package.

refer to:

Introduction to reentrant locks: https://blog.csdn.net/yanyan19880509/article/details/52345422

IBM's introduction to Lock: http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html

http://286.iteye.com/blog/2296249

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325284275&siteId=291194637