/ Java  

Java Multithreading 15: ReentrantLock

Introduction to ReentrantLock

ReentrantLock is a reentrant mutex, also known as exclusive lock.

As the name implies, ReentrantLock locks can only be held by one thread at the same time. Reentrant means that ReentrantLock locks can be acquired multiple times by the same thread.

ReentrantLock is divided into fair lock“ and unfair lock. The difference is whether the lock acquisition mechanism is fair. Lock is used to protect competing resources and prevent multiple threads from simultaneously operating on the same resources. ReentrantLock can only be acquired by one thread at the same time (when a thread acquires a lock, other threads must wait). It is through a FIFO waiting queue to manage all threads that try to acquire the lock. 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 locks are available.

ReentrantLock methods list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// Create a ReentrantLock. By default it's a unfair lock
ReentrantLock()

// Create a ReentrantLock. fair is true means it's a fair lock, fair is false means it's a unfair lock
ReentrantLock(boolean fair)

// Query the hold count of lock of the current thread
int getHoldCount()

// Return the current owner of the lock. If the lock is not owned, it will return null
protected Thread getOwner()

// Return a collection of thread that's waiting for the lock
protected Collection<Thread> getQueuedThreads()

// Return the numbber of thread that's waiting for the lock
int getQueueLength()

// Returns a collection containing those threads that may be waiting on the given condition associated with this lock.
protected Collection<Thread> getWaitingThreads(Condition condition)

// Returns an estimate of the number of threads waiting on the given condition associated with this lock.
int getWaitQueueLength(Condition condition)

// Queries whether any threads are waiting to acquire this lock.
boolean hasQueuedThread(Thread thread)

// Queries whether any threads are waiting to acquire this lock.
boolean hasQueuedThreads()

// Queries whether any threads are waiting on the given condition associated with this lock.
boolean hasWaiters(Condition condition)

// Returns true if this lock is fair.
boolean isFair()

// Return true if current thread holds this lock and false otherwise
boolean isHeldByCurrentThread()

// Queries if this lock is held by any thread.
boolean isLocked()

// Acquire the lock
void lock()

// Acquires the lock unless the current thread is interrupted
void lockInterruptibly()

// Returns a Condition instance for use with this Lock instance.
Condition newCondition()

// Acquires the lock only if it is not held by another thread at the time of invocation.
boolean tryLock()

// Try release the lock
void unlock()

ReentrantLock example

By comparing example 1 and example 2, we can clearly understand the role of lock and unlock

Example 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Depot {
// size of depot
private int size;
// Exclusive lock
private Lock lock;

public Depot() {
this.size = 0;
this.lock = new ReentrantLock();
}

public void produce(int val) {
lock.lock();
try {
size += val;
System.out.printf("%s produce(%d) --> size=%d\n", Thread.currentThread().getName(), val, size);
} finally {
lock.unlock();
}
}

public void consume(int val) {
lock.lock();
try {
size -= val;
System.out.printf("%s consume(%d) <-- size=%d\n", Thread.currentThread().getName(), val, size);
} finally {
lock.unlock();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Producer {
private Depot depot;

public Producer(Depot depot) {
this.depot = depot;
}

// Create a thread to add products to the depot
public void produce(final int val) {
new Thread(() -> depot.produce(val)).start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Customer {
private Depot depot;

public Customer(Depot depot) {
this.depot = depot;
}

// Create a thread to consume products from the depot
public void consume(final int val) {
new Thread(() -> depot.consume(val)).start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class LockTest1 {
public static void main(String[] args) {
Depot mDepot = new Depot();
Producer mPro = new Producer(mDepot);
Customer mCus = new Customer(mDepot);

mPro.produce(60);
mPro.produce(120);
mCus.consume(90);
mCus.consume(150);
mPro.produce(110);
}
}

Results:

1
2
3
4
5
Thread-0 produce(60) --> size=60
Thread-1 produce(120) --> size=180
Thread-2 consume(90) <-- size=90
Thread-3 consume(150) <-- size=-60
Thread-4 produce(110) --> size=50
  • Depot is a warehouse. Produce() can produce products into the warehouse, and consume() can consume the products from the warehouse. Mutually exclusive access to the warehouse is achieved through an exclusive lock: before operating (production/consumption) products in the warehouse, the warehouse will be locked by lock() and unlocked by unlock() after the operation.
  • Producer is a producer class. Call the producer() function in Producer to create a new thread to produce products in the warehouse.
  • Customer is a consumer class. Call the consume() function in Customer to create a new thread to consume products in the warehouse.
  • In the main thread main, we will create a new producer mPro and a new consumer mCus. They produce/consume products separately from the warehouse.
  • Based on the amount of production/consumption in main, the final product left in the warehouse should be 50. The running result is in line with our expectations!

There are two problems with this model:

  1. In reality, the capacity of the warehouse cannot be negative. However, the warehouse capacity in this model can be negative, which contradicts reality!
  2. In reality, the capacity of the warehouse is limited. However, the capacity in this model is indeed unlimited!

We will talk about how to solve these two problems.

Now, let’s look at a simple example 2. By comparing “example 1” and “example 2”, we can more clearly understand the purpose of lock() and unlock().

Example 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DepotNoLock {
// size of depot
private int size;

public DepotNoLock() {
this.size = 0;
}

public void produce(int val) {
size += val;
System.out.printf("%s produce(%d) --> size=%d\n", Thread.currentThread().getName(), val, size);
}

public void consume(int val) {
size -= val;
System.out.printf("%s consume(%d) <-- size=%d\n", Thread.currentThread().getName(), val, size);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Producer {
private DepotNoLock depot;

public Producer(DepotNoLock depot) {
this.depot = depot;
}

// Create a thread to add products to the depot
public void produce(final int val) {
new Thread(() -> depot.produce(val)).start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Customer {
private DepotNoLock depot;


public Customer(DepotNoLock depot) {
this.depot = depot;
}

// Create a thread to consume products from the depot
public void consume(final int val) {
new Thread(() -> depot.consume(val)).start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class LockTest2 {
public static void main(String[] args) {
DepotNoLock mDepot = new DepotNoLock();
Producer mPro = new Producer(mDepot);
Customer mCus = new Customer(mDepot);

mPro.produce(60);
mPro.produce(120);
mCus.consume(90);
mCus.consume(150);
mPro.produce(110);
}
}

Results:

1
2
3
4
5
Thread-0 produce(60) --> size=-60
Thread-4 produce(110) --> size=50
Thread-2 consume(90) <-- size=-60
Thread-1 produce(120) --> size=-60
Thread-3 consume(150) <-- size=-60

Example 2 removes the lock on the basis of example 1. In example 2, the final remaining product in the warehouse is -60 instead of the 50 as we expected. The reason is that we have not implemented mutually exclusive access to the warehouse.

Example 3

In example 3, we use Condition to solve the two problems in example 1: The size of the warehouse cannot be negative and the capacity of the warehouse is limited.

The solution to this problem is through Condition. Condition needs to be used in conjunction with Lock: through the await() method in Condition, the thread can be blocked [similar to wait()]. Through the signal() method of Condition, the thread can be woken up [similar to notify()].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class Depot {
// capacity of warehouse
private final int capacity;
// actual size of warehouse
private int size;
// exclusive lock
private final Lock lock;
// produce condition (not reach full capacity)
private final Condition fullCondition;
// consume condition (not empty size)
private final Condition emptyCondition;

public Depot(int capacity) {
this.capacity = capacity;
this.size = 0;
this.lock = new ReentrantLock();
this.fullCondition = lock.newCondition();
this.emptyCondition = lock.newCondition();
}

public void produce(int val) {
lock.lock();
try {
// left means the number need to be produced
int left = val;
while (left > 0) {
// when warehouse is full, wait consumer to consume product
while (size >= capacity)
fullCondition.await();

// get the actual number to be produced (the number to be added to the warehouse)
int inc = (size + left) > capacity ? (capacity - size) : left;
size += inc;
left -= inc;
System.out.printf("%s produce(%3d) --> left=%3d, inc=%3d, size=%3d\n", Thread.currentThread().getName(), val, left, inc, size);

// notify consumer to consume product
emptyCondition.signal();
}
} catch (InterruptedException ignored) {
} finally {
lock.unlock();
}
}

public void consume(int val) {
lock.lock();
try {
// left means the number need to be consumed
int left = val;
while (left > 0) {
// when warehouse size is 0, wait producer to produce product
while (size <= 0)
emptyCondition.await();

// get the actual number to be consumed (the number to be consumed from the warehouse)
int dec = Math.min(size, left);
size -= dec;
left -= dec;
System.out.printf("%s consume(%3d) <-- left=%3d, dec=%3d, size=%3d\n", Thread.currentThread().getName(), val, left, dec, size);

fullCondition.signal();
}
} catch (InterruptedException ignored) {
} finally {
lock.unlock();
}
}

public String toString() {
return "capacity:" + capacity + ", actual size:" + size;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Producer {
private final Depot depot;

public Producer(Depot depot) {
this.depot = depot;
}

// Create a thread to add products to the depot
public void produce(final int val) {
new Thread(() -> depot.produce(val)).start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Customer {
private final Depot depot;

public Customer(Depot depot) {
this.depot = depot;
}

// Create a thread to consume products from the depot
public void consume(final int val) {
new Thread(() -> depot.consume(val)).start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class MainClass {
public static void main(String[] args) {
Depot mDepot = new Depot(100);
Producer mPro = new Producer(mDepot);
Customer mCus = new Customer(mDepot);

mPro.produce(60);
mPro.produce(120);
mCus.consume(90);
mCus.consume(150);
mPro.produce(110);
}
}

Results:

1
2
3
4
5
6
7
8
9
10
11
Thread-1 produce(120) --> left= 20, inc=100, size=100
Thread-2 consume( 90) <-- left= 0, dec= 90, size= 10
Thread-3 consume(150) <-- left=140, dec= 10, size= 0
Thread-4 produce(110) --> left= 10, inc=100, size=100
Thread-3 consume(150) <-- left= 40, dec=100, size= 0
Thread-4 produce(110) --> left= 0, inc= 10, size= 10
Thread-3 consume(150) <-- left= 30, dec= 10, size= 0
Thread-1 produce(120) --> left= 0, inc= 20, size= 20
Thread-3 consume(150) <-- left= 10, dec= 20, size= 0
Thread-0 produce( 60) --> left= 0, inc= 60, size= 60
Thread-3 consume(150) <-- left= 0, dec= 10, size= 50