Chapter 9 - GUI Applications

Almost all GUI toolkits, including Swing, are implemented as a single-threaded subsystem. All GUI activity is confined to a single dedicated event dispatch thread. Attempts at multi-threaded GUIs suffered from deadlocks and race conditions. User actions manifest as events that bubble up from the GUI component to the application. Application initiated actions bubble down from the application code to the GUI components. Hence, GUI components are often accessed in opposite order, creating ripe conditions for deadlocks.

Tasks that execute in the event thread must complete quickly. Otherwise the UI will hang.

In Swing, GUI objects are kept consistent not by synchronization, but by thread confinement. They must NOT be accessed from any other thread.

A few Swing methods are thread-safe:

SwingUtilities.isEventDispatchThread

SwingUtilities.invokeLater - schedules a Runnable to be executed on the event thread.

SwingUtilities.invokeAndWait - callable only from a non-GUI thread. Schedules Runnable to be executed on GUI thread and waits for it complete

methods to enqueue a repaint or revalidate request on the event queue.

methods for adding/removing event listeners.

Short-running tasks can be run directly on the GUI thread. For long running tasks, use Executors.newCachedThreadPool().

Use Future, so that tasks can be easily cancelled. The task must be coded so that it is responsive to interruption.

SwingWorker class provides support for cancellation, progress indication, completion notification. So, we don't have to implement our own using FutureTask and Executor.

Data models must be thread-safe if they are to be accessed from the GUI thread.

A program that has both a presentation-domain and an application domain data model is said to have a split-model design.

presentation data model is confined to event thread. Application domain data model is thread-safe and is shared between the application and GUI threads.

presentation model registers listeners with the application model so that it can be notified of updates. Presentation model can be updated from the application model by sending a snapshot of the current state or via incremental updates.

Chapter 10 - Avoiding Liveness Hazards

Unlike database systems, JVM does not do deadlock detection or recovery

A program will be free of lock-ordering deadlocks if all threads acquire the needed locks in a fixed global order.

The order of locks acquired by a thread may depend on external input. Hence static analysis alone is not sufficient to avoid lock-ordering deadlocks.

An alternative is to induce an ordering on locks by using System.identityHashCode. Order lock acquisition by the hash code of the lock object.

In the extremely unlike scenario where the hash codes of two lock objects are equal, acquire a third "tie" lock before trying to acquire the original two locks. The tie lock can be a global lock. Since hash collisions are infrequent, the tie lock won't introduce a concurrency bottleneck.

If the lock objects (say bank Accounts) have a unique key, lock acquisition can be ordered by the key, and there is no need for the tie-lock.

Multiple locks may not always acquired in the same method. Hence, it is not easy to detect lock-ordering deadlocks. Watch out for invocation of alien methods while holding a lock.

Calling a method with no locks held is called an open call. Liveness of a program can be more easily analyzed if all calls are open.

Use synchronized blocks within methods to guard shared state, instead of making the entire method synchronized.

In cases where loss of atomicity of the synchronized method is unacceptable, we need to construct application level protocols. For example, when shutting down a service, lock for just long enough to mark the service as shutting down, and wait for existing tasks to complete without holding the lock. Since the service is marked as shutting down, no new tasks will start.

In addition to deadlocking waiting for locks, threads can also deadlock waiting for resources like database connections.

If you must acquire multiple locks, lock ordering must be part of your design. Minimize number of locks needed. Document ordering policy.

Timed locks offered by the Lock class are another option for detecting and recovering from deadlocks. The tryLock() method returns failure if timeout expires. It can return failure even if no deadlock occurred, but the thread just took a long time due to some other reason.

JVM prints out deadlock information in thread dumps. To trigger a thread dump, send SIGQUIT (kill -3) to the JVM. Explicit Lock objects are not clearly shown in a thread dump.

CPU cycle starvation can be caused by inappropriate use of thread priorities, or by executing infinite loops with locks held.

Avoid setting thread priorities as they are platform-dependent and can cause liveness issues. Set lower priorities only for truly background tasks, that can improve the responsiveness of foreground tasks.

Livelock - thread is not blocked, but cannot make progress because it keeps retrying an operation that will always fail. For example, when a code bug is triggered when processing a particular input, and that input is re-queued for processing by over-eager error handling code. An unrecoverable error is being mistakenly being treated as a recoverable one. Solution for some forms for livelocks is to introduce randomness into the retry.