Java EE Pitfalls #1: Ignore the default lock of a @Singleton
EJB Singleton Beans were introduced by the EJB 3.1 specification and are often used to store cached data. This means, we try to improve the performance of our application by using a Singleton. In general, this works quite well. Especially if there are not too many calls in parallel. But it changes if we ignore the default lock and the number of parallel calls increases.
Sensible defaults
Let’s start with some Java code and see how the sensible default of the lock works out. The following snippet shows a simple EJB Singleton with a counter and two methods. method1 writes the current value of the counter to the log and method2 counts from 0 to 100.
@Singleton @Remote(SingletonRemote.class) public class DefaultLock implements SingletonRemote { Logger logger = Logger.getLogger(DefaultLock.class.getName()); private int counter = 0; @Override public void method1() { this.logger.info("method1: " + counter); } @Override public void method2() throws Exception { this.logger.info("start method2"); for (int i = 0; i < 100; i++) { counter++; logger.info("" + counter); } this.logger.info("end method2"); } }
As you can see, there is no lock defined. What do you expect to see in the log file, if we call both methods in parallel?
2014-06-24 21:18:51,948 INFO [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 5) method1: 0 2014-06-24 21:18:51,949 INFO [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 4) start method2 2014-06-24 21:18:51,949 INFO [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 4) 1 2014-06-24 21:18:51,949 INFO [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 4) 2 2014-06-24 21:18:51,950 INFO [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 4) 3 ... 2014-06-24 21:18:51,977 INFO [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 4) 99 2014-06-24 21:18:51,977 INFO [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 4) 100 2014-06-24 21:18:51,978 INFO [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 4) end method2 2014-06-24 21:18:51,978 INFO [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 6) method1: 100 2014-06-24 21:18:51,981 INFO [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 7) method1: 100 2014-06-24 21:18:51,985 INFO [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 8) method1: 100 2014-06-24 21:18:51,988 INFO [blog.thoughts.on.java.singleton.lock.DefaultLock] (EJB default - 9) method1: 100
OK, that might be a little unexpected, the default is a container managed write lock on the entire Singleton. This is a good default to avoid concurrent modifications of the attributes. But it is a bad default if we want to perform read-only operations. In this case, the serializationion of the method calls will result in a lower scalability and in a lower performance under high load.
How to avoid it?
The answer to that question is obvious, we need to take care of the concurrency management. As usual in Java EE, there are two ways to handle it. We can do it ourself or we can ask the container to do it.
Bean Managed Concurrency
I do not want to go into too much detail regarding Bean Managed Concurrency. It is the most flexible way to manage concurrent access. The container allows the concurrent access to all methods of the Singleton and you have to guard its state as necessary. This can be done by using synchronized and volatile. But be careful, quite often this is not as easy as it seems.
Container Managed Concurrency
The Container Managed Concurrency is much easier to use but not as flexible as the bean managed approach. But in my experience, it is good enough for common use cases.
As we saw in the log, container managed concurrency is the default for an EJB Singleton. The container sets a write lock for the entire Singleton and serializes all method calls.
We can change this behavior and define read and write locks on method and/or class level. This can be done by annotating the Singleton class or the methods with @javax.ejb.Lock(javax.ejb.LockType). The LockType enum provides the values WRITE and READ to define an exclusive write lock or a read lock.
The following snippet shows how to set the Lock of method1 and method2 to LockType.READ.
@Singleton @Remote(SingletonRemote.class) public class ReadLock implements SingletonRemote { Logger logger = Logger.getLogger(ReadLock.class.getName()); private int counter = 0; @Override @Lock(LockType.READ) public void method1() { this.logger.info("method1: " + counter); } @Override @Lock(LockType.READ) public void method2() throws Exception { this.logger.info("start method2"); for (int i = 0; i < 100; i++) { counter++; logger.info("" + counter); } this.logger.info("end method2"); } }
As already mentioned, we could achieve the same by annotating the class with @Lock(LockType.READ) instead of annotating both methods.
OK, if everything works as expect it, both methods should be accessed in paralel. So lets have a look at the log file.
2014-06-24 21:47:13,290 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 10) method1: 0 2014-06-24 21:47:13,291 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) start method2 2014-06-24 21:47:13,291 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 1 2014-06-24 21:47:13,291 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 2 2014-06-24 21:47:13,291 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 3 ... 2014-06-24 21:47:13,306 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 68 2014-06-24 21:47:13,307 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 69 2014-06-24 21:47:13,308 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 3) method1: 69 2014-06-24 21:47:13,310 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 70 2014-06-24 21:47:13,310 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 71 ... 2014-06-24 21:47:13,311 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 76 2014-06-24 21:47:13,311 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 77 2014-06-24 21:47:13,312 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 2) method1: 77 2014-06-24 21:47:13,312 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 78 2014-06-24 21:47:13,312 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 79 ... 2014-06-24 21:47:13,313 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 83 2014-06-24 21:47:13,313 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 84 2014-06-24 21:47:13,314 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 5) method1: 84 2014-06-24 21:47:13,316 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 85 2014-06-24 21:47:13,316 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 86 2014-06-24 21:47:13,317 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 87 2014-06-24 21:47:13,318 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 88 2014-06-24 21:47:13,318 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 6) method1: 89 2014-06-24 21:47:13,318 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 89 2014-06-24 21:47:13,319 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 90 ... 2014-06-24 21:47:13,321 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 99 2014-06-24 21:47:13,321 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) 100 2014-06-24 21:47:13,321 INFO [blog.thoughts.on.java.singleton.lock.ReadLock] (EJB default - 1) end method2
Conclusion
At the beginning of this article, we found out that Java EE uses a container managed write lock as default. This results in a serialized processing of all method calls and lowers the scalability and performance of the application. This is something we need to have in mind when implementing an EJB Singleton.
We had a look at the two exisiting options to control the concurrency management: the Bean Managed Concurrency and the Container Managed Concurrency.
We used the container managed approach to define a read lock for both methods of our singleton. This is not as flexible as the bean managed approach, but it is much easier to use and sufficient in most of the cases. We just need to provide an annotation and the container will handle the rest.
Reference: | Java EE Pitfalls #1: Ignore the default lock of a @Singleton from our JCG partner Thorben Janssen at the Some thoughts on Java (EE) blog. |