Difference between revisions of "AllOrNothingLocks"

From CSE231 Wiki
Jump to navigation Jump to search
(Added instruction to return an enum if the transaction fails)
 
(67 intermediate revisions by 5 users not shown)
Line 1: Line 1:
 +
credit for this assignment: [http://jcip.net/ Java Concurrency in Practice], Ben Choi, Miles Cushing, and Dennis Cosgrove
 +
 +
=Motivation=
 +
In addition to [[Ordered_Locks|Ordered Locks]] we can employ an all-or-nothing strategy for acquiring locks for mutual exclusion.
 +
 +
We gain experience with explicit locks and trying to acquire them and responding appropriately to success and failure.
 +
 
=Background=
 
=Background=
Check out the [[Lock_Ordering#Background]] studio.
+
Check out the [[Lock_Ordering#Background|lock ordering]] exercise.
  
In Java, locks are a synchronization tool that exists to ensure whatever is protected by the lock is only accessed one thread at a time, as to avoid a possible data race. We will explore two different types of locks: intrinsic locks and lock objects.
+
In Java, locks are a synchronization tool that exists to ensure whatever is protected by the lock is only accessed one thread at a time, as to avoid a possible data race. In this exercise we will explore the second type of locks: explicitly created <code>Lock</code> objects from the <code>java.util.concurrent.locks</code> package.
  
Every object has an intrinsic lock associated with the object, but lock objects are explicitly defined and instantiated locks which can be called to lock or unlock. Intrinsic locks are called with the <code>synchronized</code> keyword. Lock objects are acted upon just like any other object, but we are mostly interested in the <code>lock()</code> method, the <code>tryLock()</code> method, and the <code>unlock()</code> method. For this studio, we will specifically use the <code>ReentrantLock</code> object, a class which implements the general <code>Lock</code> interface.
+
Every object has an intrinsic lock associated with the object, but <code>Lock</code> objects are explicitly defined and instantiated locks which can be called to lock or unlock. Lock objects are acted upon just like any other object, but we are mostly interested in the <code>lock()</code> method, the <code>tryLock()</code> method, and the <code>unlock()</code> method. For this exercise, we will specifically use the <code>ReentrantLock</code> object, a class which implements the general <code>Lock</code> interface.
  
 
To demonstrate how to avoid both data races and deadlock issues, we will create a method designed to transfer money between two bank accounts. Two parties should be able to asynchronously and continuously transfer money between each other without a data race (courtesy of locks) or deadlock (something you will address).
 
To demonstrate how to avoid both data races and deadlock issues, we will create a method designed to transfer money between two bank accounts. Two parties should be able to asynchronously and continuously transfer money between each other without a data race (courtesy of locks) or deadlock (something you will address).
  
The two possible ways to avoid deadlock, which we will address in this studio, are: lock ordering and tryLocking.
+
For this approach, we will make use of the <code>tryLock()</code> method in order to attempt to acquire an object’s lock. If the object’s lock is not available at the time, we will continue to try to acquire the lock until it finally succeeds, after which we can successfully transfer funds from one bank account to another. In this approach, an asynchronous pair of transfers from A to B and B to A will not lead to deadlock as the two processes will give up on acquiring the lock until it becomes available, at which point it will finally acquire the lock. This approach does not make use of ordering, but simply relies on making lock attempts give up if the desired lock is not immediately available (and then try again later).
  
With lock ordering, we will control the order in which locks are acquired for two bank accounts using unique identifying values associated with the bank accounts. In this way, a transfer from bank account A to B paired with a simultaneous transfer from B to A will not lead to a scenario in which A is waiting for B’s lock forever while B is waiting for A’s lock forever, as both calls will always attempt to go for A’s lock before B’s or vice versa. For a general object, we might use the object’s hashcode as the unique identifying value, but for this assignment, we will use the account number associated with the bank account (which is guaranteed to be unique, unlike hashcodes).
+
==Java Util Concurrent==
 +
Refer to Oracle's official documentation for more information:
 +
* [https://docs.oracle.com/javase/tutorial/essential/concurrency/newlocks.html Lock Objects Tutorial]
 +
* [https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/package-summary.html package java.util.concurrent.locks]
  
How would you handle the case where two objects were equal?
+
*[https://docs.oracle.com/javase/tutorial/java/nutsandbolts/while.html while loops]
<spoiler show="spoiler" hide="spoiler">Use a third object</spoiler>
+
*[https://docs.oracle.com/javase/tutorial/essential/exceptions/finally.html finally]
 +
*[https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#yield-- Thread yield]
  
In the other approach, we will make use of the <code>tryLock()</code> method in order to attempt to acquire an object’s lock. If the object’s lock is not available at the time, we will continue to try to acquire the lock until it finally succeeds, after which we can successfully transfer funds from one bank account to another. In this approach, an asynchronous pair of transfers from A to B and B to A will not lead to deadlock as the two processes will give up on acquiring the lock until it becomes available, at which point it will finally acquire the lock. This approach does not make use of ordering, but simply relies on making lock attempts give up if the desired lock is not immediately available (and then try again later).
+
* [https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/Lock.html-- Lock]
 +
** [https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/Lock.html#tryLock-- tryLock]
 +
** [https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/Lock.html#unlock--- unlock]
  
Refer to Oracle's official documentation regarding [https://docs.oracle.com/javase/tutorial/essential/concurrency/newlocks.html lock] [https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/package-summary.html objects], [https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantLock.html reentrant locks], and [https://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html intrinsic locks/synchronization] for more information.
+
* [https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantLock.html--- ReentrantLock]
 +
** [https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantLock.html#isHeldByCurrentThread--- isHeldByCurrentThread ]
  
=Where to Start=
+
* [https://docs.oracle.com/javase/8/docs/api/java/util/ListIterator.html ListIterator<E>]
 +
** [https://docs.oracle.com/javase/8/docs/api/java/util/ListIterator.html#hasNext-- boolean hasNext()]
 +
** [https://docs.oracle.com/javase/8/docs/api/java/util/ListIterator.html#next-- E next()]
 +
** [https://docs.oracle.com/javase/8/docs/api/java/util/ListIterator.html#hasPrevious-- boolean hasPrevious()]
 +
** [https://docs.oracle.com/javase/8/docs/api/java/util/ListIterator.html#previous-- E previous()]
  
Navigate to the '''locks''' directory to get started. From there, the classes you will need to modify can be found under the <code>locking.studio</code> directory. The '''core''' directory is composed of utility and building block classes we created for you. Take a look at these classes to get a better understanding of how to use them for your assignment. It does not matter which approach you choose to do first.
+
=Somewhat Dated Demo Videos=
 +
{{CollapsibleYouTube|All Or Nothing Locks|<youtube>0MLAdhm1RCA</youtube>}}
  
 +
{{CollapsibleYouTube|Bank Account Try Lock|<youtube>yaB2f8CS5wQ</youtube>}}
  
==Lock Ordering==
+
{{CollapsibleYouTube|AllOrNothingLockUtils|<youtube>cI_wrhfT7Vw</youtube>}}
  
Navigate to <code>LockOrdering.java</code>. As mentioned in the background section, we will attempt to prevent deadlock using a lock ordering algorithm of your making. Essentially, whenever an operation needs to use two separate locks, make sure that the locks are always acquired in a fixed order no matter how the operation is called. The best way to do this is to compare the hashcodes of the two objects and enforce an order based on those values. However, this can lead to a rare issue in which the two objects are unique, but have the same hashcode. In this case, you will need to lock a third placeholder object before locking any of the other objects. To avoid this issue entirely, however, we have assigned unique bank account numbers to each instance of the bank account object. Thus, you will not need to worry about this edge case.
+
{{CollapsibleYouTube|Chef|<youtube>rKIEDf1nHVg</youtube>}}
  
A couple of notes and common issues:
 
  
*You can use the ReentranLocks associated with the sender and recipient, but we recommend simply using intrinsic locks for this method. In other words, use <code>synchronized(sender)</code> or <code>synchronized(recipient)</code> to make your code simpler.
+
=Code To Implement=
*We recommend you call the <code>BankAccount.checkBalanceAndTransfer()</code> method for the sake of simplicity. This method will check that the sender and recipient are not the same person and that the sender has enough money in her account to send the specified amount to the recipient.
+
As mentioned in the background section, this implementation will avoid deadlock by giving up on acquiring a lock if it is not immediately available before coming back to it later (when it might actually be available). This implementation will need to make use of the ReentrantLocks associated with the sender and recipient bank account objects. More specifically, it will have to make use of the <code>tryLock()</code> method. For an example of how to format a tryLock, look below:
*Use the <code>accountNumber()</code> method to get the unique account ID of a given bank account.
 
*Your implementation will have to make use of a nested <code>synchronized</code> block (one <code>synchronized</code> call within another).
 
  
==Try Locking==
+
  <nowiki>Lock lock = ...
 
 
Navigate to <code>TryLocking.java</code>. As mentioned in the background section, this implementation will avoid deadlock by giving up on acquiring a lock if it is not immediately available before coming back to it later (when it might actually be available). This implementation will need to make use of the ReentrantLocks associated with the sender and recipient bank account objects. More specifically, it will have to make use of the <code>tryLock()</code> method. For an example of how to format a tryLock, look below:
 
 
 
  <nowiki>Lock lock = new ReentrantLock();
 
 
if (lock.tryLock()) {
 
if (lock.tryLock()) {
 
try {
 
try {
Line 47: Line 60:
 
}
 
}
 
}</nowiki>
 
}</nowiki>
 +
 +
===BankAccountLockTrying===
 +
 +
{{CodeToImplement|BankAccountLockTrying|attemptToTransferMoney|lock.allornothing.bank.exercise}}
 +
 +
{{ThreadSafe|public static TransferResult attemptToTransferMoney(AccountWithReentrantLock sender, AccountWithReentrantLock recipient, int amount)}}
  
 
A couple of notes and common issues:
 
A couple of notes and common issues:
  
*<code>tryLock()</code> will immediately give up and return false if the lock is not available when it is called. Thus, you will need to construct a loop that will continuously run until the desired lock is available. You can exit out of this loop in two ways: with the break keyword or by returning a valid value for the method.
+
*<code>tryLock()</code> will grab the lock if it can. There's no need to call <code>lock()</code> after <code>tryLock()</code>.
*Anytime you lock a lock object, you must use a <code>try/finally</code> block to unlock the lock.
+
*We recommend you call the <code>TransferUtils.checkBalanceAndTransfer(sender, receiver, amount)</code> method for the sake of simplicity. This method will check that the sender and recipient are not the same people and that the sender has enough money in her account to send the specified amount to the recipient.
*We recommend you call the <code>BankAccount.checkBalanceAndTransfer()</code> method for the sake of simplicity. This method will check that the sender and recipient are not the same people and that the sender has enough money in her account to send the specified amount to the recipient.
+
*Anytime you lock a lock object, you should/must use a <code>try/finally</code> block to unlock the lock.
 
*Your implementation will have to make use of a nested <code>try/finally</code> block (one <code>try/finally</code> call within another).
 
*Your implementation will have to make use of a nested <code>try/finally</code> block (one <code>try/finally</code> call within another).
 +
*If at any point, you are unable to complete the transaction, your program should first, give up any locks it's currently holding (via the finally's), and then return the UNABLE_TO_ATTEMPT_AT_THIS_TIME TransferResult enum.
 +
 +
===AllOrNothingLockUtils===
 +
{{CodeToImplement|AllOrNothingLockUtils|tryLockAll<br>unlockAll<br>runWithAllLocksOrDontRunAtAll|lock.allornothing.util.exercise}}
 +
 +
tryLockAll and unlockAll are private methods which will be used in the implementation of runWithAllLocksOrDontRunAtAll.
 +
 +
====tryLockAll====
 +
{{Sequential|private static boolean tryLockAll(List<ReentrantLock> locks)}}
 +
 +
tryLockAll should... well... [https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/Lock.html#tryLock() try to acquire] all of the locks.
 +
 +
* if all of the locks are successfully locked, true should be returned (with all of the locks held).
 +
* if all of the locks cannot be successfully locked, the locks that were acquired should be unlocked.
 +
 +
{{Alert|A lock cannot be unlocked from a thread that doesn't hold the lock.<br/>Consider using the [https://docs.oracle.com/javase/8/docs/api/java/util/ListIterator.html ListIterator] interface to move forward and backward through the provided list of ReentrantLocks.}}
 +
 +
====unlockAll====
 +
{{Sequential|private static void unlockAll(List<ReentrantLock> locks)}}
 +
 +
unlockAll should... well... [https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantLock.html#unlock() unlock] all of the locks.  This method should only be called when all of the locks are held by the current thread.  Thus, it should throw an [https://docs.oracle.com/javase/7/docs/api/java/lang/IllegalMonitorStateException.html IllegalMonitorStateException] if one of the locks is not held by the current thread.
 +
 +
* If a lock is held by the current thread, it should be unlocked.
 +
* If a lock is not held by the current thread, an [https://docs.oracle.com/javase/7/docs/api/java/lang/IllegalMonitorStateException.html IllegalMonitorStateException] should be thrown.  Note: this perhaps sounds more complicated than it is.  Calling an [https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantLock.html#unlock() unlock] on an unheld Lock will throw an [https://docs.oracle.com/javase/7/docs/api/java/lang/IllegalMonitorStateException.html IllegalMonitorStateException].  Therefore, a simple for each loop that unlocks all of the locks without checking to see if they are held will produce the correct behavior.
 +
 +
====runWithAllLocksOrDontRunAtAll====
 +
{{ThreadSafe|public static boolean runWithAllLocksOrDontRunAtAll(List<ReentrantLock> locks, Runnable body)}}
 +
 +
runWithAllLocksOrDontRunAtAll should use tryLockAll and unlockAll to produce the desired behavior.
 +
 +
* If all of the locks are successfully acquired, the [https://docs.oracle.com/javase/7/docs/api/java/lang/Runnable.html Runnable] body should be [https://docs.oracle.com/javase/7/docs/api/java/lang/Runnable.html#run() run] while the locks are being held, and then the locks should be released before returning true indicating success.
 +
* If all of the locks cannot be acquired, false should be returned indicating failure to run the specified body.
 +
 +
====runWithAllLocks====
 +
This version of runWithAllLocks is a convenience method which repeatedly tries to runWithAllLocksOrDontRunAtAll until it works, invoking the failureAction each time it fails.
 +
 +
{{CollapsibleCode|runWithAllLocks(locks, body, failureAction)|
 +
<syntaxhighlight lang="java">
 +
public static void runWithAllLocks(List<ReentrantLock> locks, Runnable body, InterruptibleRunnable failureAction) throws InterruptedException {
 +
while (true) {
 +
if (runWithAllLocksOrDontRunAtAll(locks, body)) {
 +
break;
 +
} else {
 +
failureAction.run();
 +
}
 +
}
 +
}
 +
</syntaxhighlight>}}
 +
 +
====runWithAllLocks====
 +
This version of runWithAllLocks is a convenience method which calls the more general version of runWithAllLocks, specifying to [https://docs.oracle.com/javase/7/docs/api/java/lang/Thread.html#yield() yield] the current Thread as its failure action.
 +
 +
{{CollapsibleCode|runWithAllLocks(locks, body)|
 +
<syntaxhighlight lang="java">
 +
public static void runWithAllLocks(List<ReentrantLock> locks, Runnable body) throws InterruptedException {
 +
runWithAllLocks(locks, body, () -> {
 +
if (Thread.currentThread().isInterrupted()) {
 +
throw new InterruptedException();
 +
}
 +
Thread.yield();
 +
});
 +
}
 +
</syntaxhighlight>}}
 +
 +
===Chef===
 +
 +
{{CodeToImplement|Chef|attemptToPrepare|lock.allornothing.kitchen.exercise}}
 +
 +
{{Sequential|public static boolean attemptToPrepare(Recipe recipe)}}
 +
 +
A couple of notes:
 +
 +
*An object of class Recipe contains a list of Appliances that it needs in order to complete the item. An object of class Appliance has a lock that you can use to make sure the method remains thread-safe.
 +
*Use the <code>recipe.prepare()</code> method in order to actually execute the recipe. Caution: this method will throw an <code>IllegalThreadStateException()</code> if it is called without all the locks being held by the same thread that called the create method.
 +
* Since it is unknown how many locks each recipe will need beforehand, make use of the <code>AllOrNothingLockUtils.runWithAllLocksOrDontRunAtAll(List<ReentrantLock> locks, Runnable body)</code> method you wrote earlier.
 +
 +
Perhaps a higher-order function from earlier in the semester will be useful here.
 +
 +
{{Spoiler|[[Higher-order_Functions_Map_And_Reduce_Assignment#map|map higher-order function]]}}
 +
 +
=Testing Your Solution=
 +
{{TestSuite|_TryLockingTestSuite|lock.allornothing.exercise}}
 +
 +
=Pledge, Acknowledgments, Citations=
 +
{{Pledge|all-or-nothing-locks}}

Latest revision as of 17:01, 5 April 2023

credit for this assignment: Java Concurrency in Practice, Ben Choi, Miles Cushing, and Dennis Cosgrove

Motivation

In addition to Ordered Locks we can employ an all-or-nothing strategy for acquiring locks for mutual exclusion.

We gain experience with explicit locks and trying to acquire them and responding appropriately to success and failure.

Background

Check out the lock ordering exercise.

In Java, locks are a synchronization tool that exists to ensure whatever is protected by the lock is only accessed one thread at a time, as to avoid a possible data race. In this exercise we will explore the second type of locks: explicitly created Lock objects from the java.util.concurrent.locks package.

Every object has an intrinsic lock associated with the object, but Lock objects are explicitly defined and instantiated locks which can be called to lock or unlock. Lock objects are acted upon just like any other object, but we are mostly interested in the lock() method, the tryLock() method, and the unlock() method. For this exercise, we will specifically use the ReentrantLock object, a class which implements the general Lock interface.

To demonstrate how to avoid both data races and deadlock issues, we will create a method designed to transfer money between two bank accounts. Two parties should be able to asynchronously and continuously transfer money between each other without a data race (courtesy of locks) or deadlock (something you will address).

For this approach, we will make use of the tryLock() method in order to attempt to acquire an object’s lock. If the object’s lock is not available at the time, we will continue to try to acquire the lock until it finally succeeds, after which we can successfully transfer funds from one bank account to another. In this approach, an asynchronous pair of transfers from A to B and B to A will not lead to deadlock as the two processes will give up on acquiring the lock until it becomes available, at which point it will finally acquire the lock. This approach does not make use of ordering, but simply relies on making lock attempts give up if the desired lock is not immediately available (and then try again later).

Java Util Concurrent

Refer to Oracle's official documentation for more information:

Somewhat Dated Demo Videos

Video: All Or Nothing Locks  
Video: Bank Account Try Lock  
Video: AllOrNothingLockUtils  
Video: Chef  


Code To Implement

As mentioned in the background section, this implementation will avoid deadlock by giving up on acquiring a lock if it is not immediately available before coming back to it later (when it might actually be available). This implementation will need to make use of the ReentrantLocks associated with the sender and recipient bank account objects. More specifically, it will have to make use of the tryLock() method. For an example of how to format a tryLock, look below:

Lock lock = ...
if (lock.tryLock()) {
	try {
		doSomething();
	} finally {
		lock.unlock();
	}
}

BankAccountLockTrying

class: BankAccountLockTrying.java Java.png
methods: attemptToTransferMoney
package: lock.allornothing.bank.exercise
source folder: student/src/main/java

method: public static TransferResult attemptToTransferMoney(AccountWithReentrantLock sender, AccountWithReentrantLock recipient, int amount) Sequential.svg (thread-safe required)

A couple of notes and common issues:

  • tryLock() will grab the lock if it can. There's no need to call lock() after tryLock().
  • We recommend you call the TransferUtils.checkBalanceAndTransfer(sender, receiver, amount) method for the sake of simplicity. This method will check that the sender and recipient are not the same people and that the sender has enough money in her account to send the specified amount to the recipient.
  • Anytime you lock a lock object, you should/must use a try/finally block to unlock the lock.
  • Your implementation will have to make use of a nested try/finally block (one try/finally call within another).
  • If at any point, you are unable to complete the transaction, your program should first, give up any locks it's currently holding (via the finally's), and then return the UNABLE_TO_ATTEMPT_AT_THIS_TIME TransferResult enum.

AllOrNothingLockUtils

class: AllOrNothingLockUtils.java Java.png
methods: tryLockAll
unlockAll
runWithAllLocksOrDontRunAtAll
package: lock.allornothing.util.exercise
source folder: student/src/main/java

tryLockAll and unlockAll are private methods which will be used in the implementation of runWithAllLocksOrDontRunAtAll.

tryLockAll

method: private static boolean tryLockAll(List<ReentrantLock> locks) Sequential.svg (sequential implementation only)

tryLockAll should... well... try to acquire all of the locks.

  • if all of the locks are successfully locked, true should be returned (with all of the locks held).
  • if all of the locks cannot be successfully locked, the locks that were acquired should be unlocked.
Attention niels epting.svg Alert:A lock cannot be unlocked from a thread that doesn't hold the lock.
Consider using the ListIterator interface to move forward and backward through the provided list of ReentrantLocks.

unlockAll

method: private static void unlockAll(List<ReentrantLock> locks) Sequential.svg (sequential implementation only)

unlockAll should... well... unlock all of the locks. This method should only be called when all of the locks are held by the current thread. Thus, it should throw an IllegalMonitorStateException if one of the locks is not held by the current thread.

  • If a lock is held by the current thread, it should be unlocked.
  • If a lock is not held by the current thread, an IllegalMonitorStateException should be thrown. Note: this perhaps sounds more complicated than it is. Calling an unlock on an unheld Lock will throw an IllegalMonitorStateException. Therefore, a simple for each loop that unlocks all of the locks without checking to see if they are held will produce the correct behavior.

runWithAllLocksOrDontRunAtAll

method: public static boolean runWithAllLocksOrDontRunAtAll(List<ReentrantLock> locks, Runnable body) Sequential.svg (thread-safe required)

runWithAllLocksOrDontRunAtAll should use tryLockAll and unlockAll to produce the desired behavior.

  • If all of the locks are successfully acquired, the Runnable body should be run while the locks are being held, and then the locks should be released before returning true indicating success.
  • If all of the locks cannot be acquired, false should be returned indicating failure to run the specified body.

runWithAllLocks

This version of runWithAllLocks is a convenience method which repeatedly tries to runWithAllLocksOrDontRunAtAll until it works, invoking the failureAction each time it fails.

runWithAllLocks(locks, body, failureAction)  
public static void runWithAllLocks(List<ReentrantLock> locks, Runnable body, InterruptibleRunnable failureAction) throws InterruptedException {
	while (true) {
		if (runWithAllLocksOrDontRunAtAll(locks, body)) {
			break;
		} else {
			failureAction.run();
		}
	}
}

runWithAllLocks

This version of runWithAllLocks is a convenience method which calls the more general version of runWithAllLocks, specifying to yield the current Thread as its failure action.

runWithAllLocks(locks, body)  
public static void runWithAllLocks(List<ReentrantLock> locks, Runnable body) throws InterruptedException {
	runWithAllLocks(locks, body, () -> {
		if (Thread.currentThread().isInterrupted()) {
			throw new InterruptedException();
		}
		Thread.yield();
	});
}

Chef

class: Chef.java Java.png
methods: attemptToPrepare
package: lock.allornothing.kitchen.exercise
source folder: student/src/main/java

method: public static boolean attemptToPrepare(Recipe recipe) Sequential.svg (sequential implementation only)

A couple of notes:

  • An object of class Recipe contains a list of Appliances that it needs in order to complete the item. An object of class Appliance has a lock that you can use to make sure the method remains thread-safe.
  • Use the recipe.prepare() method in order to actually execute the recipe. Caution: this method will throw an IllegalThreadStateException() if it is called without all the locks being held by the same thread that called the create method.
  • Since it is unknown how many locks each recipe will need beforehand, make use of the AllOrNothingLockUtils.runWithAllLocksOrDontRunAtAll(List<ReentrantLock> locks, Runnable body) method you wrote earlier.

Perhaps a higher-order function from earlier in the semester will be useful here.

Testing Your Solution

class: _TryLockingTestSuite.java Junit.png
package: lock.allornothing.exercise
source folder: testing/src/test/java

Pledge, Acknowledgments, Citations

file: all-or-nothing-locks-pledge-acknowledgments-citations.txt

More info about the Honor Pledge