Зміст курсу
Multithreading in Java
Multithreading in Java
Lock and Condition
In Java, standard synchronization is based on the synchronized
keyword and built-in monitor objects. However, in some cases, synchronized may not be sufficient, especially when greater flexibility in thread management is required.
General Description
The Lock
interface and the Condition
interface, introduced in the java.util.concurrent.locks
package, provide advanced thread management capabilities.
In this picture, you can see that the first thread captures the lock using the lock()
method, and at this moment, another thread cannot capture the same lock. As soon as all the code inside the lock is executed, it will call the unlock()
method and release the lock. Only after that can the second thread capture the lock.
Difference
The difference between these two interfaces is that the Lock
implementations are a high-level alternative to the synchronized block, and the Condition
interface implementations are an alternative to the notify()
/wait()
methods. Both of these interfaces are part of the java.util.concurrent.locks
package.
Real-Life Examples
Imagine you are managing a queue for registration for an event. To prevent overflow and ensure proper seat allocation, you need to use blocking mechanisms and conditions to keep the flow of new registrations waiting until a free seat becomes available.
ReentrantLock Class
The ReentrantLock
class from the java.util.concurrent.locks
package is an implementation of the Lock
interface. It provides functionality for explicitly managing locks.
The main methods of ReentrantLock:
lock()
: Captures a lock;unlock()
: Releases the lock;tryLock()
: Tries to capture the lock and returns true if the capture is successful;tryLock(long timeout, TimeUnit unit)
: Tries to capture the lock for the specified time;newCondition()
: Creates a condition for the currentLock
.
Main
package com.example; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Main { // Creating a ReentrantLock object private final Lock lock = new ReentrantLock(); private int count = 0; // Method to increment the `count` variable public void increment() { lock.lock(); // Acquiring the `lock` try { count++; System.out.println("Count incremented to: " + count); } finally { lock.unlock(); // Releasing the `lock` } } public static void main(String[] args) { Main example = new Main(); // Runnable task to call the increment method Runnable task = example::increment; // Starting multiple threads to execute the task for (int i = 0; i < 5; i++) { new Thread(task).start(); } } }
As you can see, in the increment()
method we use locking with Lock
. When a thread enters the method it captures the lock in lock.lock()
and then executes the code and then in the finally block we release the lock in lock.unlock()
so that other threads can enter.
We release the lock in the finally
block for a reason, it's just that this block is almost always executed, even with exceptions, except when we terminate the program.
Condition Interface
We can only create a Condition
with a binding to a specific Lock
implementation. For this reason, the Condition
methods will only impact the locking of that particular Lock
implementation.
Main
private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition();
The main methods of Condition:
await()
: Waits for a signal from another thread;signal()
: Unblocks one thread waiting on a condition;signalAll()
: Unblocks all threads waiting on the condition.
Main
private final ReentrantLock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); private boolean ready = false; public void waitForCondition() throws InterruptedException { lock.lock(); // Acquire the `lock` try { while (!ready) { // Check if the condition is not met System.out.println("Waiting..."); // Print a waiting message condition.await(); // Wait for the condition to be signaled } System.out.println("Condition met!"); // Print a message when the condition is met } finally { lock.unlock(); // Release the `lock` } }
The waitForCondition()
method blocks the thread until the ready
variable becomes true, which signals that the condition has been met. When the condition is met, the thread continues running, displaying the message “Condition met!”
Note
When the
await()
method is called, the thread is paused and also releases the lock it has captured. When the thread wakes up, it should capture the lock again and only then it will start executing!
Code Example
Now let's look at an example of using ReentrantLock
and Condition
to manage registration for an event:
A Short Clip from the Video
Locking with ReentrantLock: The register()
method captures the lock with lock.lock()
to prevent multiple threads from executing code at the same time.
Condition with Condition: If there are no available spaces, the thread calls spaceAvailable.await()
to wait until space is available.
Release Lock: When a thread has released space using the cancel()
method, it calls spaceAvailable.signalAll()
to notify all waiting threads.
Exception handling: Using try-finally
blocks ensures that the lock is released even if an exception occurs.
Conclusion
The use of
Lock
andCondition
in Java allows more flexible control over threads and synchronization than the traditional synchronized mechanism. This is especially useful in complex scenarios where more precise control over threads and wait conditions is required.
Дякуємо за ваш відгук!