Nov 6, 2017 6:00:02 PM | Java Exception Handling- IllegalMonitorStateException

A look at the Java IllegalMonitorStateException, with a code sample illustrating how to handle such exceptions within applications.

Moving along through our in-depth Java Exception Handling series, today we'll get into the IllegalMonitorStateException. The IllegalMonitorStateException is thrown when a thread has been instructed to wait for an object's monitor that the specified thread does not have ownership of.

Throughout this article we'll explore the IllegalMonitorStateException in greater detail, starting with where it resides in the overall Java Exception Hierarchy. We'll also look at a functional code sample that illustrates how multithreading and concurrent waiting might be implemented, and how IllegalMonitorStateExceptions can appear if such an implementation has some small flaws in it. Let's get crackin'!

The Technical Rundown

All Java errors implement the java.lang.Throwable interface, or are extended from another inherited class therein. The full exception hierarchy of this error is:

Full Code Sample

Below is the full code sample we'll be using in this article. It can be copied and pasted if you'd like to play with the code yourself and see how everything works.

// Main.java
package io.airbrake;

import io.airbrake.utility.Logging;

public class Main {

static Main main = null;

public static void main(String[] args)
{
main = new Main();
}

private Main()
{
Logging.lineSeparator("DUAL THREADING");
DualThreadingTest();

Logging.lineSeparator("DUAL THREADING w/ OWNERSHIP");
DualThreadingWithOwnershipTest();
}

private void DualThreadingTest() {
try {
// Create manager.
ThreadingManager manager = new ThreadingManager();

// Create runners and add to manager.
manager.addRunner(new Runner("primary"));
manager.addRunner(new Runner("secondary"));

// Create threads, set runners, and add to manager.
manager.addThread(new Thread(manager.getRunner("primary"), "primary"));
manager.addThread(new Thread(manager.getRunner("secondary"), "secondary"));

// Start threads.
manager.getThread("primary").start();
manager.getThread("secondary").start();
} catch (IllegalMonitorStateException exception) {
// Output expected IllegalMonitorStateExceptions.
Logging.log(exception);
} catch (Exception exception) {
// Output unexpected Exceptions.
Logging.log(exception, false);
}
}

private void DualThreadingWithOwnershipTest() {
try
{
// Create manager.
ThreadingManager manager = new ThreadingManager();

// Create runners and add to manager.
manager.addRunner(new Runner("primary"));
manager.addRunner(new Runner("secondary"));

// Create threads, set runners, and add to manager.
manager.addThread(new Thread(manager.getRunner("primary"), "primary"));
manager.addThread(new Thread(manager.getRunner("secondary"), "secondary"));

// Set runner thread ownership.
manager.getRunner("primary").setThread(manager.getThread("primary"));
manager.getRunner("secondary").setThread(manager.getThread("secondary"));

// Start threads.
manager.getThread("primary").start();
manager.getThread("secondary").start();
} catch (IllegalMonitorStateException exception) {
// Output expected IllegalMonitorStateExceptions.
Logging.log(exception);
} catch (Exception exception) {
// Output unexpected Exceptions.
Logging.log(exception, false);
}
}
}

// ThreadingManager.java
package io.airbrake;

import java.util.ArrayList;
import java.util.Objects;

public class ThreadingManager {
private ArrayList<Runner> runners = new ArrayList<>();
private ArrayList<Thread> threads = new ArrayList<>();

ThreadingManager() { }

void addRunner(Runner runner) {
this.runners.add(runner);
}

void addThread(Thread thread) {
threads.add(thread);
}

public Runner getRunner(int index) {
return runners.get(index);
}

Runner getRunner(String name) {
for (Runner runner : runners) {
if (Objects.equals(runner.getName(), name)) {
return runner;
}
}
return null;
}

public ArrayList<Runner> getRunners() {
return runners;
}

public Thread getThread(int index) {
return threads.get(index);
}

Thread getThread(String name) {
for (Thread thread : threads) {
if (Objects.equals(thread.getName(), name)) {
return thread;
}
}
return null;
}

public ArrayList<Thread> getThreads() {
return threads;
}

public void setRunners(ArrayList<Runner> runners) {
this.runners = runners;
}

public void setThreads(ArrayList<Thread> threads) {
this.threads = threads;
}
}

// Runner.java
package io.airbrake;

import io.airbrake.utility.Logging;

public class Runner implements Runnable
{
private String name;
private Thread thread;

Runner(String name) {
setName(name);
}

Runner(String name, Thread thread) {
setName(name);
}

String getName() {
return this.name;
}

private Thread getThread() {
return thread;
}

private void setName(String name) {
this.name = name;
}

void setThread(Thread thread) {
this.thread = thread;
}

public void run()
{
try
{
// Check for ownership thread.
if (getThread() == null) {
Logging.log(String.format("Waiting for thread %s in runner %s.", "Main", getName()));
// If no thread, wait for main thread.
Main.main.wait();
} else {
synchronized (getThread()) {
Logging.log(String.format("Waiting for thread %s in runner %s.", getThread().getName(), getName()));
// If thread, invoke wait().
getThread().wait();
}
}
} catch (IllegalMonitorStateException exception) {
// Output expected IllegalMonitorStateExceptions.
Logging.log(exception);
} catch (Exception exception) {
// Output unexpected Exceptions.
Logging.log(exception, false);
}
}
}

This code sample also uses the Logging.java class, the source of which can be found on GitHub.

When Should You Use It?

As described by the official documentation, an IllegalMonitorStateException can occur when a thread attempts to wait on an object's monitor, or to notify other threads waiting for said object's monitor, when that thread does not own the monitor in question. Put another way, if the Object.wait() method is called on an object which was not created by the current thread, an IllegalMonitorStateException will be thrown. A thread can become the owner of an object's monitor by:

  • Executing a synchronized instance method of the object.
  • Executing a code block of a synchronized statement that synchronizes on the object.
  • Executing a synchronized static method of the object, if the object is a Class type.

This is easier to understand with some code examples, but before we get into that it's worth briefly noting that the use of Object.wait() and notify() methods is generally not the go-to technique for performing multithreaded or concurrent programming in modern Java. If you're interested in how most Java developments handle concurrency, take a look at the java.util.concurrent package.

The goal with our code sample is simple: To create two threads and assign each their own Runnable object, which is an interface that implements the run() method. A Runnable instance can be passed to a new Thread instance, and the Runnable.run() method will be automatically executed by the thread once it's started.

Thus, we begin with our Runner class, which implements the Runnable interface:

package io.airbrake;

import io.airbrake.utility.Logging;

public class Runner implements Runnable
{
private String name;
private Thread thread;

Runner(String name) {
setName(name);
}

Runner(String name, Thread thread) {
setName(name);
}

String getName() {
return this.name;
}

private Thread getThread() {
return thread;
}

private void setName(String name) {
this.name = name;
}

void setThread(Thread thread) {
this.thread = thread;
}

public void run()
{
try
{
// Check for ownership thread.
if (getThread() == null) {
Logging.log(String.format("Waiting for thread %s in runner %s.", "Main", getName()));
// If no thread, wait for main thread.
Main.main.wait();
} else {
synchronized (getThread()) {
Logging.log(String.format("Waiting for thread %s in runner %s.", getThread().getName(), getName()));
// If thread, invoke wait().
getThread().wait();
}
}
} catch (IllegalMonitorStateException exception) {
// Output expected IllegalMonitorStateExceptions.
Logging.log(exception);
} catch (Exception exception) {
// Output unexpected Exceptions.
Logging.log(exception, false);
}
}
}

Aside from a few getters and setters, the core logic is found in the run() method. It starts by checking if the Thread thread property of the Runner exists via the getThread() method. If not, it explicitly calls the Main.main.wait() method, otherwise it calls the wait() method of the associated Thread object for this Runner instance.

The ThreadingManager class is just a helper class to make it easier to generate and keep track of Runners and Threads by storing them in private ArrayList<Runner> runners and ArrayList<Thread> threads properties. We'll also use many of the helper methods, such as getRunner(String name), to retrieve specific instances of Runners and Threads for testing purposes later on:

package io.airbrake;

import java.util.ArrayList;
import java.util.Objects;

public class ThreadingManager {
private ArrayList<Runner> runners = new ArrayList<>();
private ArrayList<Thread> threads = new ArrayList<>();

ThreadingManager() { }

void addRunner(Runner runner) {
this.runners.add(runner);
}

void addThread(Thread thread) {
threads.add(thread);
}

public Runner getRunner(int index) {
return runners.get(index);
}

Runner getRunner(String name) {
for (Runner runner : runners) {
if (Objects.equals(runner.getName(), name)) {
return runner;
}
}
return null;
}

public ArrayList<Runner> getRunners() {
return runners;
}

public Thread getThread(int index) {
return threads.get(index);
}

Thread getThread(String name) {
for (Thread thread : threads) {
if (Objects.equals(thread.getName(), name)) {
return thread;
}
}
return null;
}

public ArrayList<Thread> getThreads() {
return threads;
}

public void setRunners(ArrayList<Runner> runners) {
this.runners = runners;
}

public void setThreads(ArrayList<Thread> threads) {
this.threads = threads;
}
}

Speaking of testing, let's look at our Main class and the first test method, DualThreadingTest():

public class Main {
// ...

private void DualThreadingTest() {
try {
// Create manager.
ThreadingManager manager = new ThreadingManager();

// Create runners and add to manager.
manager.addRunner(new Runner("primary"));
manager.addRunner(new Runner("secondary"));

// Create threads, set runners, and add to manager.
manager.addThread(new Thread(manager.getRunner("primary"), "primary"));
manager.addThread(new Thread(manager.getRunner("secondary"), "secondary"));

// Start threads.
manager.getThread("primary").start();
manager.getThread("secondary").start();
} catch (IllegalMonitorStateException exception) {
// Output expected IllegalMonitorStateExceptions.
Logging.log(exception);
} catch (Exception exception) {
// Output unexpected Exceptions.
Logging.log(exception, false);
}
}

// ...
}

As you can see, our test begins with a new ThreadingManager instance, from which we create two new Runners named primary and secondary, respectively. We then create two new Threads with the same names, and retrieve the appropriate Runners from our manager instance to pass to those new Thread(...) constructor calls. As mentioned before, passing a Runnable instance (like a Runner object) to a Thread constructor ensures that the Runner's run() method is executed when the thread starts execution. Consequently, our last step is to start() both threads.

Executing the DualThreadingTest() code produces the following output, which includes throwing some IllegalMonitorStateExceptions:

------------ DUAL THREADING ------------
Waiting for thread Main in runner primary.
Waiting for thread Main in runner secondary.
[EXPECTED] java.lang.IllegalMonitorStateException
[EXPECTED] java.lang.IllegalMonitorStateException

Since we didn't explicitly tell our Runner instances which specific Thread instance each was assigned, they both attempted to call the wait() method of the Main.main object, as seen in this snippet from the Runner.run() method:

// ...

// Check for ownership thread.
if (getThread() == null) {
Logging.log(String.format("Waiting for thread %s in runner %s.", "Main", getName()));
// If no thread, wait for main thread.
Main.main.wait();
} else {
synchronized (getThread()) {
Logging.log(String.format("Waiting for thread %s in runner %s.", getThread().getName(), getName()));
// If thread, invoke wait().
getThread().wait();
}
}

// ...

At the most basic level, every object in Java has an intrinsic lock associated with it. Anytime a thread needs exclusive access to that object it must acquire ownership of the object's intrinsic lock. Once exclusive access requirements are finished, it can release the lock. In some scenarios, like the one we created above, a thread may attempt an action on an object for which it doesn't have an exclusive lock, which can cause an IllegalMonitorStateException in some situations.

One solution is to use the built-in synchronized statement, which specifies an object that will be providing the intrinsic lock to the code block within the statement. You can see an example of this in the else { ... } block of our Runner.run() method above. By using the synchronized (getThread()) statement, we give the code within the synchronized block access to the lock of the associated thread of this Runner instance (as acquired by getThread()). It's worth noting that it's usually best programming practice to use a local variable and retrieve the Thread instance via getThread() one time, then reuse it throughout the code. However, in this case, using a local variable within a synchronized statement can be tricky to control, since synchronization may occur out of order (slower or faster) than code outside of it that actually produced the local variable. Thus, it's safer to make the explicit call every time in this case.

To test this behavior we have the DualThreadingWithOwnershipTest() method, which is similar to DualThreadingTest(), except we explicitly assign Thread instances to the Runner instances before start() is called for both Threads:

private void DualThreadingWithOwnershipTest() {
try
{
// Create manager.
ThreadingManager manager = new ThreadingManager();

// Create runners and add to manager.
manager.addRunner(new Runner("primary"));
manager.addRunner(new Runner("secondary"));

// Create threads, set runners, and add to manager.
manager.addThread(new Thread(manager.getRunner("primary"), "primary"));
manager.addThread(new Thread(manager.getRunner("secondary"), "secondary"));

// Set runner thread ownership.
manager.getRunner("primary").setThread(manager.getThread("primary"));
manager.getRunner("secondary").setThread(manager.getThread("secondary"));

// Start threads.
manager.getThread("primary").start();
manager.getThread("secondary").start();
} catch (IllegalMonitorStateException exception) {
// Output expected IllegalMonitorStateExceptions.
Logging.log(exception);
} catch (Exception exception) {
// Output unexpected Exceptions.
Logging.log(exception, false);
}
}

As a result, this invokes the synchronized(...) statement for each appropriate thread within the Runner.run() method, ensuring our concurrency works as expected and doesn't throw any inels in the output:

----- DUAL THREADING w/ OWNERSHIP ------
Waiting for thread primary in runner primary.
Waiting for thread secondary in runner secondary.

The Airbrake-Java library provides real-time error monitoring and automatic exception reporting for all your Java-based projects. Tight integration with Airbrake's state of the art web dashboard ensures that Airbrake-Java gives you round-the-clock status updates on your application's health and error rates. Airbrake-Java easily integrates with all the latest Java frameworks and platforms like Spring, Maven, log4j, Struts, Kotlin, Grails, Groovy, and many more. Plus, Airbrake-Java allows you to easily customize exception parameters and gives you full, configurable filter capabilities so you only gather the errors that matter most.

Check out all the amazing features Airbrake-Java has to offer and see for yourself why so many of the world's best engineering teams are using Airbrake to revolutionize their exception handling practices! Try Airbrake free for 30 days.

Written By: Frances Banks