...
To
...
avoid
...
data
...
corruption
...
in
...
multithreaded
...
Java
...
programs,
...
shared
...
data
...
must
...
be
...
protected
...
from
...
concurrent
...
modifications
...
and
...
accesses.
...
Locking can
...
be
...
performed
...
at
...
the
...
object
...
level
...
using
...
synchronized methods, synchronized blocks, or the java.util.concurrent
dynamic lock objects. However, excessive use of locking can result in deadlocks.
Java neither prevents deadlocks nor requires their detection [JLS 2015]. Deadlock can occur when two or more threads request and release locks in different orders. Consequently, programs are required to avoid deadlock by acquiring and releasing locks in the same order.
Additionally, synchronization should be limited to cases where it is absolutely necessary. For example, the paint()
, dispose()
, stop()
, and destroy()
methods should never be synchronized in an applet because they are always called and used from dedicated threads. Furthermore, the Thread.stop()
and Thread.destroy()
methods are deprecated (see THI05-J. Do not use Thread.stop() to terminate threads for more information).
This rule also applies to programs that need to work with a limited set of resources. For example, liveness issues can arise when two or more threads are waiting for each other to release resources such as database connections. These issues can be resolved by letting each waiting thread retry the operation at random intervals until they successfully acquire the resource.
Noncompliant Code Example (Different Lock Orders)
This noncompliant code example can deadlock because of excessive synchronization. The balanceAmount
field represents the total balance available for a particular BankAccount
object. Users are allowed to initiate an operation that atomically transfers a specified amount from one account to another.
Code Block | ||
---|---|---|
| ||
final class BankAccount { private double balanceAmount; // Total amount in bank account BankAccount(double}} methods or blocks. However, excessive use of synchronization may result in deadlocks (See [CON08-J. Do not call alien methods or constructors that synchronize on the same object as the calling class]). According to the Java Language Specification \[[JLS 05|AA. Java References#JLS 05]\], "The Java programming language neither prevents nor requires detection of deadlock conditions." Deadlocks can arise unless each thread requests and releases locks in the same order. h2. Noncompliant Code Example This noncompliant code example can deadlock because synchronization is implemented incorrectly. Assume that an attacker has two bank accounts and is capable of requesting two {{depositAllAmount()}} operations in succession, from the two threads started in {{main()}}. {code:bgColor=#FFcccc} class BankAccount { private int balanceAmount; // Total amount in bank account private BankAccount(int balance) { this.balanceAmount = balance; } // Deposits the amount from this object instance // to BankAccount instance argument ba private synchronizedvoid void depositAllAmountdepositAmount(BankAccount ba, double amount) { System.out.println("Depositing all amount..."); synchronized (this) { synchronized (ba.balanceAmount += this.balanceAmount;) { this.balanceAmount = 0; // withdraw allif (amount from> thisbalanceAmount) instance{ ba.displayAllAmount(); // Display the new balanceAmount in ba (may cause deadlock) } private synchronized void displayAllAmount() { System.out.println(balanceAmount throw new IllegalArgumentException( "Transfer cannot be completed" ); } public static void main(String[] args) throws Exception { } final BankAccount a = new BankAccount(5000)ba.balanceAmount += amount; final BankAccount bthis.balanceAmount -= new BankAccount(6000)amount; // These two} threads correspond to two} malicious requests} triggered bypublic thestatic attacker void initiateTransfer(final BankAccount first, Thread t1 = newfinal Thread(new Runnable() { public void run() { a.depositAllAmount(b); } }); BankAccount second, final double amount) { Thread t2transfer = new Thread(new Runnable() { public void run() { b first.depositAllAmount(adepositAmount(second, amount); } }); t1.start(); t2transfer.start(); } } {code} |
Objects
...
of this class are prone to deadlock. An attacker who has two bank accounts can construct two threads that initiate balance transfers from two different BankAccount
object instances a
and b
. For example, consider the following code:
Code Block |
---|
BankAccount a = new BankAccount(5000);
BankAccount b = new BankAccount(6000);
BankAccount.initiateTransfer(a, b, 1000); // starts thread 1
BankAccount.initiateTransfer(b, a, 1000); // starts thread 2
|
Each transfer is performed in its own thread. The first thread atomically transfers the amount from a
to b
by depositing it in account b
and then withdrawing the same amount from a
. The second thread performs the reverse operation; that is, it transfers the amount from b
to a
. When executing depositAmount()
, the first thread acquires a lock on object a
. The second thread could acquire a lock on object b
before the first thread can. Subsequently, the first thread would request a lock on b
, which is already held by the second thread. The second thread would request a lock on a
, which is already held by the first thread. This constitutes a deadlock condition because neither thread can proceed.
This noncompliant code example may or may not deadlock, depending on the scheduling details of the platform. Deadlock occurs when (1) two threads request the same two locks in different orders, and (2) each thread obtains a lock that prevents the other thread from completing its transfer. Deadlock is avoided when two threads request the same two locks but one thread completes its transfer before the other thread begins. Similarly, deadlock is avoided if the two threads request the same two locks in the same order (which would happen if they both transfer money from one account to a second account) or if two transfers involving distinct accounts occur concurrently.
Compliant Solution (Private Static Final Lock Object)
This compliant solution avoids deadlock by synchronizing on a private static final lock object before performing any account transfers:
Code Block | ||
---|---|---|
| ||
final class BankAccount {
private double balanceAmount; // Total amount in bank account
private static final Object lock = new Object();
BankAccount(double balance) {
this.balanceAmount = balance;
}
// Deposits the amount from this object instance
// to BankAccount instance argument ba
private void depositAmount(BankAccount ba, double amount) {
synchronized (lock) {
if (amount > balanceAmount) {
throw new IllegalArgumentException(
"Transfer cannot be completed");
}
ba.balanceAmount += amount;
this.balanceAmount -= amount;
}
}
public static void initiateTransfer(final BankAccount first,
final BankAccount second, final double amount) {
Thread transfer = new Thread(new Runnable() {
@Override public void run() {
first.depositAmount(second, amount);
}
});
transfer.start();
}
}
|
In this scenario, deadlock cannot occur when two threads with two different BankAccount
objects try to transfer to each other's accounts simultaneously. One thread will acquire the private lock, complete its transfer, and release the lock before the other thread can proceed.
This solution imposes a performance penalty because a private static lock restricts the system to performing transfers sequentially. Two transfers involving four distinct accounts (with distinct target accounts) cannot be performed concurrently. This penalty increases considerably as the number of BankAccount
objects increase. Consequently, this solution fails to scale well.
Compliant Solution (Ordered Locks)
This compliant solution ensures that multiple locks are acquired and released in the same order. It requires a consistent ordering over BankAccount
objects. Consequently, the BankAccount
class implements the java.lang.Comparable
interface and overrides the compareTo()
method.
Code Block | ||
---|---|---|
| ||
final class BankAccount implements Comparable<BankAccount> { private double balanceAmount; // Total amount in bank account private final Object lock; private final long id; // Unique for each BankAccount private static final AtomicLong nextID = new AtomicLong(0); // Next unused ID BankAccount(double balance) { this.balanceAmount = balance; this.lock = new Object(); this.id = nextID.getAndIncrement(); } @Override public int compareTo(BankAccount ba) { return (this.id > ba.id) ? 1 : (this.id < ba.id) ? -1 : 0; } // Deposits the amount from this object instance // to BankAccount instance argument ba public void depositAmount(BankAccount ba, double amount) { BankAccount former, latter; if (compareTo(ba) < 0) { former = this; latter = ba; } else { former = ba; latter = this; } synchronized (former class {{BankAccount}} represent bank accounts. The {{balanceAmount}} field represents the total balance amount available for a particular object (bank account). A user is allowed to initiate an operation _deposit all amount_ that transfers the balance amount from one account to another. This is equivalent to closing a bank account and transferring the balance to a different (existing or new) account. An attacker may cause the program to construct two threads that initiate balance transfers from two different {{BankAccount}} object instances. The two transfers are performed from the first object to the second object and from the second object to the first. {{BankAccount}} object's instances {{a}} and {{b}} are constructed such that the first thread atomically transfers the amount from {{a}} to {{b}} by depositing the balance from {{a}} to {{b}} and withdrawing the entire balance from {{a}}. The second thread performs the reverse operation, that is, it transfers the balance from {{b}} to {{a}} and withdraws the balance from {{b}}. These operations are safe up to this point. However, the {{depositAllAmount()}} method (first thread) invokes the {{synchronized}} {{displayAllAmount()}} method on the instance of object {{b}}. The {{displayAllAmount()}} may request the monitor that is already secured by the second thread that is performing the reverse transfer. The second thread may itself be blocked waiting to enter the monitor secured by the {{displayAllAmount()}} method of the first thread that is attempting to display the balance amount of instance {{a}}. This constitutes a deadlock condition. The threads in this program request monitors in varying order depending on the interleaving of method calls. If {{Thread T1}} finishes executing before {{Thread T2}}, there are no issues because in this case, locks are acquired ad released in the proper order. Sequences where the threads alternate, such as, {{T1}}, {{T2}}, {{T1}}, {{T2}} can result in a deadlock. h2. Compliant Solution (avoid excessive synchronization) The deadlock can be avoided by declaring the {{balanceAmount}} field as {{volatile}} and removing the {{synchronized}} keyword from the {{displayAllAmount()}} method. {code:bgColor=#ccccff} private volatile int balanceAmount; //... private void displayAllAmount() { System.out.println(balanceAmount); } {code} The {{displayAllAmount()}} method does not require additional synchronization because it atomically displays the latest value for the balance amount. To be compliant, do not request a lock when it is already held by another object where that object is waiting for the requesting object to release its own lock. When this is not possible, avoid synchronizing unnecessary accesses. h2. Noncompliant Code Example This noncompliant code example consists of three integer arrays: {{distances}}, {{speeds}} and {{times}}. The distances are fixed and cannot be changed by the client. The client can pass a {{time}} array as argument to method {{getAverageSpeed()}} to find the average speed. It can also pass a {{speed}} array as argument to method {{getAverageTime()}} to find the average time taken. This allows the client to calculate the third parameter from two given parameters. For example, speed is calculated as distance/time. The example also uses an array of internal lock objects instead of intrinsic synchronization (or method synchronization) so that multiple threads do not interfere with the array elements when the arrays are being traversed. Because it is not possible to lock on primitive types, a direct lock on the array elements cannot be obtained. Instead, an array of raw {{Objects}} ({{locks}}) is used. {code:bgColor=#FFcccc} public class RecursiveTravel { final static int MAX = 20; static int[] distances = new int[MAX]; static int[] times = new int[MAX]; static int[] speeds = new int[MAX]; static Object[] locks = new Object[MAX]; static { for (int i = 0; i < MAX; i++) { distances[i] = 10; // Assuming all distances are 10 for illustrationsynchronized (latter) { if (amount > balanceAmount) { locks[i] = new Object(); // Createthrow locknew objectsIllegalArgumentException( } } double getAverageSpeed(int[] time) { "Transfer timescannot = time.clone(be completed"); return getSpeed(0, 0) / MAX;} } double getAverageTime(int[] speed) { ba.balanceAmount += amount; speeds = speed.clone() this.balanceAmount -= amount; return getTime(MAX -} 1, 0) / MAX;} } public static intvoid getSpeedinitiateTransfer(intfinal BankAccount ifirst, int speed) { // Acquire locks in nondecreasing order final BankAccount second, final double amount) { Thread if(itransfer >= MAXnew - 1Thread(new Runnable() { return speed;@Override public void run() { } synchronized(locks[i]) {first.depositAmount(second, amount); speed += distances[i]/times[i]; } }); return getSpeed(++i, speedtransfer.start(); } } int getTime(int i, int time) { // Acquire locks in nonincreasing order if(i <= -1) { return time; } synchronized(locks[i]) { time += distances[i]/speeds[i]; return getTime(--i, time); } } } {code} The {{getSpeed()}} and {{getTime()}} methods recursively calculate the sum of the speeds and times, respectively, for each distance value in the array. The implementation is deadlock prone because the recursive calls occur within the synchronized regions of these methods and acquire locks in opposite numerical orders. That is, {{getSpeed()}} requests locks from index 0 to {{MAX}} - 1 (19) whereas {{getTime()}} requests them from index {{MAX}} - 1 (19) to 0. Because of recursion, no previously acquired locks are released by either method. A deadlock occurs when two threads call these methods out of order in that, one thread calls {{getSpeed()}} while the other calls {{getTime()}} before either method has finished executing. One such execution order that causes a deadlock is shown below: {code} Thread T1 (in getTime()) acquires lock: i = 19 ... i = 0 Thread T1 (in getSpeed()) acquires lock: i = 0 ... i = 18 Thread T2 (in getTime()) acquires lock: i = 19 Thread T1 next wants, i = 19 which T2 holds Thread T2 next wants, i = 18 which T1 holds {code} {mc} // Class to make the above code run in a multi-threaded environment public class RecursiveControl implements Runnable { static RecursiveTravel t = new RecursiveTravel(); static int[] speed = new int[MAX]; static int[] time = new int[MAX]; static { for (int i = 0; i < MAX; i++) { speed[i] = 2; time[i] = 2; } } public void run() { t.getTime(speed); t.getSpeed(time); } public static void main(String[] args) throws InterruptedException { Runnable r1 = new RecursiveControl(); Runnable r2 = new RecursiveControl(); Thread c1 = new Thread(r1); Thread c2 = new Thread(r2); c1.start(); c2.start(); } } {mc} h2. Compliant Solution This compliant solution moves the recursive calls from the {{getSpeed()}} and {{getTime()}} methods to outside the {{synchronized}} block. Consequently, locks are released as soon as they are no longer needed. Also, the locks are acquired in the same order (nondecreasing) form both these methods. This eliminates potential deadlock conditions. {code:bgColor=#ccccff} public class RecursiveTravel { // ... double getAverageSpeed(int[] time) { times = time.clone(); return getSpeed(MAX - 1, 0) / MAX; } double getAverageTime(int[] speed) { speeds = speed.clone(); return getTime(MAX - 1, 0) / MAX; } int getSpeed(int i, int speed) { // Acquire locks in nondecreasing order if(i > MAX - 1) { return speed; } synchronized(locks[i]) { speed += distances[i]/times[i]; } return getSpeed(++i, speed); // Moved outside of synchronized region } int getTime(int i, int time) { // Acquire locks in nondecreasing order if(i > MAX - 1) { return time; } synchronized(locks[i]) { time += distances[i]/speeds[i]; } return getTime(++i, time); // Moved outside of synchronized region } } {code} h2. Risk Assessment Acquiring and releasing locks in the wrong order may result in deadlocks. || Rule || Severity || Likelihood || Remediation Cost || Priority || Level || | CON12- J | low | likely | high | {color:green}{*}P3{*}{color} | {color:green}{*}L3{*}{color} | h3. Automated Detection TODO h3. Related Vulnerabilities Search for vulnerabilities resulting from the violation of this rule on the [CERT website|https://www.kb.cert.org/vulnotes/bymetric?searchview&query=FIELD+KEYWORDS+contains+CON34-J]. h2. References \[[JLS 05|AA. Java References#JLS 05]\] [Chapter 17, Threads and Locks|http://java.sun.com/docs/books/jls/third_edition/html/memory.html] \[[SDN 08|AA. Java References#SDN 08]\] [Sun Developer Network Tech tips|http://java.sun.com/developer/TechTips/2000/tt0328.html] \[[MITRE 09|AA. Java References#MITRE 09]\] [CWE ID 412|http://cwe.mitre.org/data/definitions/412.html] "Unrestricted Lock on Critical Resource" ---- [!The CERT Sun Microsystems Secure Coding Standard for Java^button_arrow_left.png!|CON11-J. Do not assume that declaring an object volatile guarantees visibility of its members] [!The CERT Sun Microsystems Secure Coding Standard for Java^button_arrow_up.png!|11. Concurrency (CON)] [!The CERT Sun Microsystems Secure Coding Standard for Java^button_arrow_right.png!|CON13-J. Do not try to force thread shutdown] } |
Whenever a transfer occurs, the two BankAccount
objects are ordered so that the first
object's lock is acquired before the second
object's lock. When two threads attempt transfers between the same two accounts, they each try to acquire the first account's lock before acquiring the second account's lock. Consequently, one thread acquires both locks, completes the transfer, and releases both locks before the other thread can proceed.
Unlike the previous compliant solution, this solution permits multiple concurrent transfers as long as the transfers involve distinct accounts.
Compliant Solution (ReentrantLock
)
In this compliant solution, each BankAccount
has a java.util.concurrent.locks.ReentrantLock
. This design permits the depositAmount()
method to attempt to acquire the locks of both accounts, to release the locks if it fails, and to try again later if necessary.
Code Block | ||
---|---|---|
| ||
final class BankAccount {
private double balanceAmount; // Total amount in bank account
private final Lock lock = new ReentrantLock();
private final Random number = new Random(123L);
BankAccount(double balance) {
this.balanceAmount = balance;
}
// Deposits amount from this object instance
// to BankAccount instance argument ba
private void depositAmount(BankAccount ba, double amount)
throws InterruptedException {
while (true) {
if (this.lock.tryLock()) {
try {
if (ba.lock.tryLock()) {
try {
if (amount > balanceAmount) {
throw new IllegalArgumentException(
"Transfer cannot be completed");
}
ba.balanceAmount += amount;
this.balanceAmount -= amount;
break;
} finally {
ba.lock.unlock();
}
}
} finally {
this.lock.unlock();
}
}
int n = number.nextInt(1000);
int TIME = 1000 + n; // 1 second + random delay to prevent livelock
Thread.sleep(TIME);
}
}
public static void initiateTransfer(final BankAccount first,
final BankAccount second, final double amount) {
Thread transfer = new Thread(new Runnable() {
public void run() {
try {
first.depositAmount(second, amount);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Reset interrupted status
}
}
});
transfer.start();
}
}
|
Deadlock is impossible in this compliant solution because locks are never held indefinitely. If the current object's lock is acquired but the second lock is unavailable, the first lock is released and the thread sleeps for some specified amount of time before attempting to reacquire the lock.
Code that uses this locking strategy has behavior similar to that of synchronized code that uses the traditional monitor lock. ReentrantLock
also provides several other capabilities. For example, the tryLock()
method immediately returns false when another thread already holds the lock. Further, the java.util.concurrent.locks.ReentrantReadWriteLock
class has multiple-readers/single-writer semantics and is useful when some threads require a lock to write information while other threads require the lock to concurrently read the information.
Noncompliant Code Example (Different Lock Orders, Recursive)
The following immutable WebRequest
class encapsulates a web request received by a server:
Code Block |
---|
// Immutable WebRequest
public final class WebRequest {
private final long bandwidth;
private final long responseTime;
public WebRequest(long bandwidth, long responseTime) {
this.bandwidth = bandwidth;
this.responseTime = responseTime;
}
public long getBandwidth() {
return bandwidth;
}
public long getResponseTime() {
return responseTime;
}
}
|
Each request has a response time associated with it, along with a measurement of the network bandwidth required to fulfill the request.
This noncompliant code example monitors web requests and provides routines for calculating the average bandwidth and response time required to serve incoming requests.
Code Block | ||
---|---|---|
| ||
public final class WebRequestAnalyzer {
private final Vector<WebRequest> requests = new Vector<WebRequest>();
public boolean addWebRequest(WebRequest request) {
return requests.add(new WebRequest(request.getBandwidth(),
request.getResponseTime()));
}
public double getAverageBandwidth() {
if (requests.size() == 0) {
throw new IllegalStateException("The vector is empty!");
}
return calculateAverageBandwidth(0, 0);
}
public double getAverageResponseTime() {
if (requests.size() == 0) {
throw new IllegalStateException("The vector is empty!");
}
return calculateAverageResponseTime(requests.size() - 1, 0);
}
private double calculateAverageBandwidth(int i, long bandwidth) {
if (i == requests.size()) {
return bandwidth / requests.size();
}
synchronized (requests.elementAt(i)) {
bandwidth += requests.get(i).getBandwidth();
// Acquires locks in increasing order
return calculateAverageBandwidth(++i, bandwidth);
}
}
private double calculateAverageResponseTime(int i, long responseTime) {
if (i <= -1) {
return responseTime / requests.size();
}
synchronized (requests.elementAt(i)) {
responseTime += requests.get(i).getResponseTime();
// Acquires locks in decreasing order
return calculateAverageResponseTime(--i, responseTime);
}
}
}
|
The monitoring application is built around the WebRequestAnalyzer
class, which maintains a list of web requests using the requests
vector and includes the addWebRequest()
setter method. Any thread can request the average bandwidth or average response time of all web requests by invoking the getAverageBandwidth()
and getAverageResponseTime()
methods.
These methods use fine-grained locking by holding locks on individual elements (web requests) of the vector. These locks permit new requests to be added while the computations are still underway. Consequently, the statistics reported by the methods are accurate when they return the results.
Unfortunately, this noncompliant code example is prone to deadlock because the recursive calls within the synchronized regions of these methods acquire the intrinsic locks in opposite numerical orders. That is, calculateAverageBandwidth()
requests locks from index 0 up to requests.size()
− 1, whereas calculateAverageResponseTime()
requests them from index requests.size()
− 1 down to 0. Because of recursion, previously acquired locks are never released by either method. Deadlock occurs when two threads call these methods out of order, because one thread calls calculateAverageBandwidth()
, while the other calls calculateAverageResponseTime()
before either method has finished executing.
For example, when there are 20 requests in the vector, and one thread calls getAverageBandwidth()
, the thread acquires the intrinsic lock of WebRequest
0, the first element in the vector. Meanwhile, if a second thread calls getAverageResponseTime()
, it acquires the intrinsic lock of WebRequest
19, the last element in the vector. Consequently, deadlock results because neither thread can acquire all of the locks required to proceed with its calculations.
Note that the addWebRequest()
method also has a race condition with calculateAverageResponseTime()
. While iterating over the vector, new elements can be added to the vector, invalidating the results of the previous computation. This race condition can be prevented by locking on the last element of the vector (when it contains at least one element) before inserting the element.
Compliant Solution
In this compliant solution, the two calculation methods acquire and release locks in the same order, beginning with the first web request in the vector.
Code Block | ||
---|---|---|
| ||
public final class WebRequestAnalyzer {
private final Vector<WebRequest> requests = new Vector<WebRequest>();
public boolean addWebRequest(WebRequest request) {
return requests.add(new WebRequest(request.getBandwidth(),
request.getResponseTime()));
}
public double getAverageBandwidth() {
if (requests.size() == 0) {
throw new IllegalStateException("The vector is empty!");
}
return calculateAverageBandwidth(0, 0);
}
public double getAverageResponseTime() {
if (requests.size() == 0) {
throw new IllegalStateException("The vector is empty!");
}
return calculateAverageResponseTime(0, 0);
}
private double calculateAverageBandwidth(int i, long bandwidth) {
if (i == requests.size()) {
return bandwidth / requests.size();
}
synchronized (requests.elementAt(i)) {
// Acquires locks in increasing order
bandwidth += requests.get(i).getBandwidth();
return calculateAverageBandwidth(++i, bandwidth);
}
}
private double calculateAverageResponseTime(int i, long responseTime) {
if (i == requests.size()) {
return responseTime / requests.size();
}
synchronized (requests.elementAt(i)) {
// Acquires locks in increasing order
responseTime += requests.get(i).getResponseTime();
return calculateAverageResponseTime(++i, responseTime);
}
}
}
|
Consequently, while one thread is calculating the average bandwidth or response time, another thread cannot interfere or induce deadlock. Each thread must first synchronize on the first web request, which cannot happen until any prior calculation completes.
Locking on the last element of the vector in addWebRequest()
is unnecessary for two reasons. First, the locks are acquired in increasing order in all the methods. Second, updates to the vector are reflected in the results of the computations.
Risk Assessment
Acquiring and releasing locks in the wrong order can result in deadlock.
Rule | Severity | Likelihood | Remediation Cost | Priority | Level |
---|---|---|---|---|---|
LCK07-J | Low | Likely | High | P3 | L3 |
Automated Detection
Some static analysis tools can detect violations of this rule.
Tool | Version | Checker | Description | ||||||
---|---|---|---|---|---|---|---|---|---|
Coverity | 7.5 | LOCK_INVERSION | Implemented | ||||||
Parasoft Jtest |
| CERT.LCK07.LORD | Ensure that nested locks are ordered correctly | ||||||
ThreadSafe |
| CCE_DL_DEADLOCK | Implemented |
Related Guidelines
CWE-833, Deadlock |
Bibliography
...