Threads
By initializing and launching an object which adheres to specific implementations, we can create another thread, which is independent of the other operations described in main method, and we can execute them simultaneously.
There’re two ways to create a thread; extending Thread
class and implementing Runnable
interface.
- To extend
Thread
class:- Initialize an instance of the class which extends
Thread
class. - Call
start
method.
- Initialize an instance of the class which extends
- To implement
Runnable
interface:- Initialize an instance of the class which implements
Runnable
interface. - Initialize an instance of
Thread
class, by calling the constructor with an input of the previous instance. - Call
start
method.
- Initialize an instance of the class which implements
In either way, what we have to adhere to is overriding run
method, and describing operations in it.
Interrupts
An interrupt is an abstract indication to a thread that it should stop what it’s doing and do something else, which is issued by another thread. In the context of Java, the interrupt mechanism is implemented using an internal flag known as the interrupt status, which is configured for each thread.
We can manipulate the interrupt status through the following three methods.
- The method
Thread.interrupt
sets the interrupt status for the thread invoking it. - The static method
Thread.interrupted
returns the interrupt status of the thread where the method is invoked, and clear the status. - The method
Thread.isInterrupted
returns the interrupt status of the thread invoking the method, without changing the status.
For the interrupt mechanism to work correctly, we want interrupted thread to handle that interrupt.
How does a thread handle interrupts? If our thread only invokes methods that throw InterruptedException
, we’ll catch that with a try-catch statement. Actually, some methods declared in java.lang.Thread
are designed to clear the interrupt status of the thread where they are invoked, and throw InterruptedException
if the thread receives an interrupt.
// how to handle interrupts
class MyThread implements Runnable {
@override
public void run(){
try {
Thread.sleep(1000); // it throws InterruptedException when the thread receives an interrupt
} catch (InterruptedException e){
return; // terminate
}
}
}
On the other hand, if a thread goes a long time without invoking a method that throws InterruptedException
, we can invoke the static method Thread.interrupted
to check if it has received an interrupt.
// how to handle interrupts
while(condition){
doSomething(); // it doesn't throw InterruptedException
if(Thread.interrupted){
return; // terminate
}
}
Additionally, throwing InterruptedException
enables us to code interrupt handlings in a catch clause.
// how to handle interrupts
while(condition){
doSomething(); // it doesn't throw InterruptedException
if(Thread.interrupted){
throw new InterruptException();
}
}
Concurrency Control
Synchronization: Exclusive and Consistent Access to Shared Resources
Synchronization ensures exclusive and consistent access to the same resource when multiple threads attempt to access it simultaneously. Synchronization is achieved using an internal entity known as the intrinsic lock that every object has. A threads that needs exclusive and consistent access to an object’s fields has to acquire the object’s intrinsic lock, and release when it’s done.
As long as an object’s intrinsic lock is acquired, other threads cannot access the object’s fields. Additionally, when a thread releases an intrinsic lock, the happen-before relationship is established, which ensures that all changes made by the thread are visible to every other thread. Technically, synchronization provides two options: synchronized methods, and synchronized statements.
When a thread invokes a synchronized method, it acquires the intrinsic lock for the method’s object, and it releases the lock when it returns. As for a static synchronized method, a thread acquires the intrinsic lock for the Class object associated with the class, which enforces exclusive and consistent access to the static fields.
On the other hand, when using a synchronized statement, we have to specify an object whose lock we acquire. With synchronized statements, we can enforce synchronizing access to the object’s fields, and avoid synchronizing invoking of other object’s method in one method.
// how to use synchronized statements
void doSomething(){
synchronized(this){
foo(); // manipulate the object's fields
}
otherObject.bar(); // invoke other object's method
}
Additionally, in the following example, when invoking the method increment1
, we don’t need to acquire an intrinsic lock for the field count2
. Thus, synchronized statements are also used to improve concurrency by limiting objects for which the intrinsic lock is acquired.
// how to use synchronized statements
class Foo {
int count1 = 0;
int count2 = 0;
Object lock1 = new Object();
Object lock2 = new Object();
void increment1(){
synchronized(lock1){
count1++;
}
}
void increment2(){
synchronized(lock2){
count2++;
}
}
}
Those synchronization technologies are known for being reentrant. Reentrant synchronization ensures that synchronized code can invoke a method which contains another synchronized code with the same intrinsic lock without causing a deadlock.
Volatile: Consistent Access to Shared Resources
As we discussed, synchronization ensures exclusive and consistent access to resources. On the other hand, volatile keyword, only used for variables, ensures consistent access but doesn’t ensure its exclusiveness.
// how to use a volatile keyword
class Bar {
volatile boolean flag;
}
Guarded Blocks: Coordinating among Multiple Threads
When threads have to coordinate their operations, a well-known solution is the guarded block. In the guarded block, two threads communicate with each other through a shared variable.
In the following case, our method must not proceed until a shared variable has been set by another thread. It’s wasteful since it executes continuously while waiting.
// guarded block
void guardedFoo(){
while(!foo){}
doSomething();
}
A more efficient approach is to use the method Object.wait
and have it enclosed in a synchronized
statement or method. When a thread invokes the method Object.wait
, it suspend until another thread sends a notification that some special event has occurred by using Object.notifyAll
.
// guarded block with Object.wait and notifyAll
void synchronized guardedFoo(){
while(!foo){
try{
wait();
} catch (InterruptedException e){}
}
doSomething();
}
void synchronized notifyFoo(){
foo = true;
notifyAll();
}
Lock Objects
The package java.util.concurrent.locks
provides higher level APIs for concurrency control. Lock
objects allow us to acquire and release a lock flexibly.
The biggest advantage of Lock
objects over synchronized methods or statements is their ability to back out of an attempt to acquire a lock, which is also known as an effective way to avoid making deadlocks. There’re well-known methods as follows:
Lock.lock
acquires the lock if it’s available. If it’s not available, the thread will wait until the lock acquired by another thread is released.Lock.unlock
release the lock.Lock.tryLock
tries to acquire the lock without waiting. It returns true if it acquires the lock successfully, and false otherwise.
// how to use java.util.concurrent.locks.Lock
class LockSample {
private final Lock lock = new ReentrantLock();
// example 1
// wait until the thread acquires the lock
void method1(){
lock.lock();
try {
// do something
} finally {
lock.unlock();
}
}
// example 2
// try to lock without waiting
void method2(){
if(lock.tryLock()){
try {
// do something
} finally {
lock.unlock();
}
}
}
// example 3
// wait until the thread acquires the lock for the specified time
void method3(){
if(lock.tryLock(1000, TimeUnit.MILLISECOND)){
try {
// do something
} finally {
lock.unlock();
}
} else {
// time out
}
}
}
Reference
- Official tutorials
- https://docs.oracle.com/javase/tutorial/essential/concurrency/runthread.html
- https://docs.oracle.com/javase/tutorial/essential/concurrency/interrupt.html
- https://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html
- https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html
- https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html
- https://docs.oracle.com/javase/tutorial/essential/concurrency/newlocks.html
- Official API references
- Articles that helped me learn about java.util.concurrent.locks.Lock