Java Concurrency: The Lock interface
Previously we implemented a thread safe counter using synchronized. We would like to swift from synchronized blocks to something more flexible with more features, this is were locks are of use. On this blog we will focus on Java’s Lock interface.
The Lock interface supports three forms of lock acquisition interruptible, non-interruptible, and timed.
We can acquire locks between threads. A thread will wait until the lock is released from the thread holding the lock:
@Test void lock() throws InterruptedException { Thread withDelayedLock = new Thread(() -> { lock.lock(); log.info("Acquired delayed"); lock.unlock(); }); lock.lock(); withDelayedLock.start(); Thread.sleep(500); log.info("Will release"); lock.unlock(); withDelayedLock.join(); }
Since the threads blocks on the lock we might as well interrupt the thread. In this case we can lock lockInterruptibly, and apply any logic in case of an Interrupt:
@Test void lockInterruptibly() throws InterruptedException { Thread withDelayedLock = new Thread(() -> { try { lock.lockInterruptibly(); } catch (InterruptedException e) { log.error("interrupted while waiting",e); } }); lock.lock(); withDelayedLock.start(); withDelayedLock.interrupt(); lock.unlock(); withDelayedLock.join(); }
A thread can also try to acquire a lock which is already acquired, and exit immediately instead of blocking.
@Test void tryLock() throws InterruptedException { Thread withDelayedLock = new Thread(() -> { boolean locked = lock.tryLock(); assertFalse(locked); }); lock.lock(); withDelayedLock.start(); Thread.sleep(500); lock.unlock(); withDelayedLock.join(); }
Also a time period can be specified until the lock is acquired.
@Test void tryLockTime() throws InterruptedException { Thread withDelayedLock = new Thread(() -> { try { boolean locked = lock.tryLock(100, TimeUnit.MILLISECONDS); assertFalse(locked); } catch (InterruptedException e) { throw new RuntimeException(e); } }); lock.lock(); withDelayedLock.start(); Thread.sleep(500); lock.unlock(); withDelayedLock.join(); }
Another thing of importance with locks is memory.
From the documentation
All {@code Lock} implementations <em>must</em> enforce the same * memory synchronization semantics as provided by the built-in monitor * lock, as described in * Chapter 17 of * <cite>The Java Language Specification</cite>: * <ul>* <li>A successful {@code lock} operation has the same memory * synchronization effects as a successful <em>Lock</em> action. *</li> <li>A successful {@code unlock} operation has the same * memory synchronization effects as a successful <em>Unlock</em> action. *</li> </ul>
In order for the results to be flushed on the main memory we need to use lock and unlock.
Take the following example
private Lock lock = new ReentrantLock(); private String threadAcquired = "main"; @Test void wrongMemoryVisibility() throws InterruptedException { Thread withDelayedLock = new Thread(() -> { lock.lock(); try { threadAcquired = "delayed"; System.out.println("Acquired on delayed"); Thread.sleep(500); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } }); lock.lock(); try { withDelayedLock.start(); Thread.sleep(500); } finally { lock.unlock(); } while (true) { System.out.println("Currently acquired " + threadAcquired); if (threadAcquired.equals("delayed")) { break; } } withDelayedLock.join(); threadAcquired = "main"; }
We print the variable threadAcquired by the main thread while it is changed by the thread with a Delayed lock.
After a few runs a situation where the threadAcquired variable has a stale value on the main thread will appear.
Acquired on delayed Currently acquired main
We did this on purpose, on a real world problem we should not access variables like threadAcquired without proper synchronisation, for example we should have acquired the lock first, this way the memory would be synchronised and we would have an up to date value.
@Test void correctMemoryVisibility() throws InterruptedException { Thread withDelayedLock = new Thread(() -> { lock.lock(); try { threadAcquired = "delayed"; System.out.println("Acquired on delayed"); Thread.sleep(500); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } }); lock.lock(); try { withDelayedLock.start(); Thread.sleep(500); } finally { lock.unlock(); } while (true) { lock.lock(); try { System.out.println("Currently acquired " + threadAcquired); if (threadAcquired.equals("delayed")) { break; } } finally { lock.unlock(); } } withDelayedLock.join(); }
That’s all for now, we will later proceed on different type of locks and the Condition interface.
Published on Java Code Geeks with permission by Emmanouil Gkatziouras, partner at our JCG program. See the original article here: Java Concurrency: The Lock interface Opinions expressed by Java Code Geeks contributors are their own. |