Summary
When thinking about threading issues in Java it helps to separate the concerns based on whether the concurrency issue is in the component (the application), or in the container (the application context, the framework, or the application server).
I am a bit hesitant about putting the "pattern" label on this separation of concerns, but I was not certain what other label would be more descriptive. The main point is that if you are a component writer, your concerns are very different from those of a container or framework writer. It helps greatly simplify thinking about threading issues if you separate them along these lines. I make this point several times, because I believe in the value of repetition, and because this note is not about how to design and to code for threading - there are hundreds of resources on the web, in articles, and in books that do a far better job. But none of these resources, to my mind, do a proper job of separating the concerns in a way that is beneficial to the typical application developer, whose main task is to deliver business functionality, and not infrastructure design. I maintain that if an application developer masters the concurrency design literature, they will be diverted from delivering business functionality and they would be better suited to develop frameworks or design new containers, or work for companies that develop application infrastructure products.
The Container/Component Separation of Concerns
Java threading issues, and designing for multi-threading, poses a different problem for the application programmer than it does for the system programmer. An application programmer is mainly concerned with designing and implementing a business application. He or she has enough to deal with in understanding the business requirements, the domain model, data handling, and presentation handling, in a way that satisfies a business need. Infrastructure issues, and threading is an infrastructure issue, intrude and seem to be "out of scope" for the application developer.
Still, the application developer cannot dismiss threading concerns by relegating them to the infrastructure, and declaring, "these are issues taken care of by my framework, I don't need to be concerned with them". He is half right and half wrong, in taking this stance. Handling threading is half an infrastructure concern and half application design concern. But exactly how? Where is the separation of concerns, and how do the responsibilities divide?
This is what I will explore in this article.
To do this we will create a couple of classes to simulate the interplay between the application and the infrastructure. We will call the infrastructure class Container, and the application class Component. You can use names like ApplicationConenxt, and Application, or ServletContainer, and Servlet. The model is the same. Some container holds a reference to your application object. It instantiates your application object (one or more instances), and it also creates a thread (or pulls one from a thread pool). Inside the thread's run method work is delegated some method (a doWork() method) in your component. The division of labor is that the container is responsible for managing the lifecycle of the thread and for picking the next one to run. The component writer is responsible for the doWork() method of his component and for ensuing that his component "plays nice" with other components running in other threads in the container, i.e., it is thread safe.
Now how does this division of labor help the application programmer handle the threading issues? It does so by relieving him of the concern of creating threads and managing thread pools and managing allocating requests for work and handing them off to worker threads.
Lets look at code to make all this concrete.
Example 1: Stateless components
File: example1/Container.java
package example1;
// Working with stateless components
// No shared state
// No race conditions, no synchronization issues
public class Container {
StatelessComponent component = new StatelessComponent();
public void handleRequests() {
// Request handling is delegated to a component
Runnable r = new Runnable () {
public void run () {
component.businessMethod(100);
}
};
// create new threads for arriving requests
// usually from the thread pool
// we will just simulate with creating via constructor
Thread t1 = new Thread (r, "Thread 1");
t1.start();
Thread t2 = new Thread (r, "Thread 2");
t2.start();
// ... more threads created as requests are handled
// following same pattern
}
}
File: example1/StatelessComponent.java
package example1;
public class StatelessComponent {
public void businessMethod (int bizArg) {
print ("entering businessMethod");
doWork();
print ("Leaving businessMethod");
}
private void doWork() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
private void print(String s) {
String threadName = Thread.currentThread().getName();
System.out.println("StatelessComponent - "+threadName + ": " + s);
}
}
File: example1/Driver.java
package example1;
public class Driver {
public static void main (String [] args) {
System.out.println("Exmaple 1. Stateless components");
System.out.println("Container Starting Up");
Container container = new Container();
container.handleRequests();
waitForShutDownCommand();
System.out.println("Container Shutting Down");
}
private static void waitForShutDownCommand() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
}
}
Result of Driver Run:
Exmaple 1. Stateless components
Container Starting Up
StatelessComponent - Thread 1: entering businessMethod
StatelessComponent - Thread 2: entering businessMethod
StatelessComponent - Thread 1: Leaving businessMethod
StatelessComponent - Thread 2: Leaving businessMethod
Container Shutting Down
Notice how Threads 1 and 2 are running concurrently, but they don't interfere with each other. They don't share state and they don't need to be synchronized. The component writer (the application programmer) does not need to do anything in the component to deal with concurrency.
Example 2: Stateful Components with Shared State.
The stateful component has a field value that must not be interfered with while the component is "doing work". The component writer (the application programmer) has to make sure any critical section (that reads or updates the shared variable is protected from interference from another thread running the same code). This is what is meant by the component is "thread safe".
We will examine example 2 without doing anything to guarantee thread safety, and we can observe what happens with the state of the shared variable.
File: example2/Container.java
Same code as example1. The application programmer does not have the luxury to re-write infrastructure code, or tamper with how the container internals run.
File: example2/StatefulComponet.java
package example2;
public class StatefullComponent {
int sharedVar = 1;
public void businessMethod (int bizArg) {
print ("entering businessMethod - shared var = " + sharedVar);
doWork();
print ("leaving businessMethod - shared var = " + sharedVar);
}
private void doWork() {
try {
sharedVar++;
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
private void print(String s) {
String threadName = Thread.currentThread().getName();
System.out.println("StatefullComponent - "+threadName + ": " + s);
}
}
Example 2: Driver Run
Example 2: Stateful Components. Shared variables not synchronized
Container Starting Up
StatefullComponent - Thread 2: entering businessMethod - shared var = 1
StatefullComponent - Thread 1: entering businessMethod - shared var = 1
StatefullComponent - Thread 2: leaving businessMethod - shared var = 3
StatefullComponent - Thread 1: leaving businessMethod - shared var = 3
Container Shutting Down
Notice that both components enter the the business method with the same value for the shared variable. There is a "lost update" for the second component. It does not see the update made by component 1. But during the run of both components the shared variable goes through all the updates made by all the components! So there is a race condition, and a corruption of data problem.
Example 3: Synchronizing the work of the business methods on shared variables
File: example3/StatefulComponent.java
package example2;
public class StatefullComponent {
int sharedVar = 1;
public synchronized void businessMethod (int bizArg) {
print ("entering businessMethod - shared var = " + sharedVar);
doWork();
print ("leaving businessMethod - shared var = " + sharedVar);
}
private void doWork() {
try {
sharedVar++;
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
private void print(String s) {
String threadName = Thread.currentThread().getName();
System.out.println("StatefullComponent - "+threadName + ": " + s);
}
}
Example 3: Driver Run
Example 2: Stateful Components. Shared variables synchronized
Container Starting Up
StatefullComponent - Thread 1: entering businessMethod - shared var = 1
StatefullComponent - Thread 1: leaving businessMethod - shared var = 2
StatefullComponent - Thread 2: entering businessMethod - shared var = 2
StatefullComponent - Thread 2: leaving businessMethod - shared var = 3
Container Shutting Down
Notice now there is no overlap in the execution of the businessMethod. Thread 2 has to wait till Thread 1 is done, and is guaranteed to see Thread 1's updates to the shared variable.
We can do more experiments, and study more concurrency and synchronization issues, by continuing with different designs of the component class. This is the main point of this article: the application programmer need not be concerned with the container design, or the creation and management of threads, but does need to know how to make access to critical sections of his components thread safe.
This separation of concerns should make it easier to an application developer to make sense out of the literature on concurrency, without feeling that she needs to become an expert in designing and writing multi-threaded code frameworks that belong in system programming and infrastructure.
There is a lot more to concurrency and synchronization - and there are many good books and articles. My purpose here was not tp survey best practices and best theory and patterns - this is well documented in other places. My purpose is to only emphasize that the application developer needs a different focus than the framework and infrastructure developer while studying and designing for concurrency.
One of the best books on Java concurrency (Java Concurrency in Practice) covers the new concurrency features of Java SE 5. It covers many of the classes and interfaces of java.util.concurrent, but it does so in a manner that is very frustrating for the Java application programmer. About half the book is about designs and patterns for safety of a component, the other half is about the structures useful to a framework or container developer. The book's intended audience, it seems, is the system or infrastructure developer. Most application developers will throw their hands up in frustration, and miss on benefitting from a great book.
My advice, and this is the whole point of this blog, is to separate the component thread safety design parts from the container threading management parts. In another note, I will present such a division, and show how the book can be made much more useful had the editors chosen to structure the book around the component/container seperation of concerns pattern that I advocate here.
Recent Comments