Lazy vs. eager instantiation in Java: Which is better?

Uncategorized

[]

When instantiating Java objects that are expensive in terms of resource usage, we don’t want to have to instantiate them every time we use them. It’s far better for performance to have a ready-to-use instance of the object that we can share across the system. In this case, the lazy instantiation strategy works very well.

Lazy instantiation has its drawbacks, however, and in some systems, a more eager approach is better. In eager instantiation, we usually instantiate the object once as soon as the application is started. Neither approach is all good or all bad: they are different. Each one works best in certain kinds of scenarios.

This article introduces you to these two ways to instantiate your Java objects. You’ll see code examples and then test what you’ve learned with a Java code challenge. We’ll also discuss the pros and cons of lazy instantiation versus eager instantiation.

A naive approach to lazy instantiation

Let’s start with a look at the naive way to create a single instance and share it in the system:

public static HeroesDB heroesDB; // #A private SingletonNaiveApproach() {} // #B public HeroesDB getHeroesDB() { // #C if (heroesDB == null) { // #D heroesDB = new HeroesDB(); // #E } return heroesDB; // #F } static class HeroesDB { } }

Here’s what’s happening in the code:

  • To start (#A), we declare a static inner class, HeroesDB. We declare the variable as static, and it can be shared in the application.
  • Next (#B), we create a private constructor to avoid direct instantiation from outside of this class. Therefore, we are obliged to use the getHeroes() method to get an instance.
  • In the next line (#C), we see the method that will effectively return the instance from HeroesDB.
  • Next (#D), we check whether the heroesDB instance is null. If that’s true, we will create a new instance. Otherwise, we do nothing.
  • Finally (#F), we return the heroesDB object instance.

This approach works for small applications. However, in a large multithreaded application with many users, chances are that there will be data collision. In that case, the object will probably be instantiated more than once, even though we have the null check. Let’s explore further why this happens.

Understanding race conditions

A race condition is a situation where two or more threads compete concurrently for the same variable, which can cause unexpected results.

In a large, multithreaded application, many processes run in parallel and concurrently. In this type of application, it is possible for one thread to be asking if an object is null at the same time that another thread instantiates that null object. In that case, we have a race condition, which could lead to duplicate instances.

We can fix this issue by using the synchronized keyword:

public class SingletonSynchronizedApproach { public static HeroesDB heroesDB; private SingletonSynchronizedApproach() {} public synchronized HeroesDB getHeroesDB() { if (heroesDB == null) { heroesDB = new HeroesDB(); } return heroesDB; } static class HeroesDB { } }

This code solves the problem with threads having conflicts in the getHeroesDB(). However, we are synchronizing the whole method. That might compromise performance because only one thread at a time will be able to access the entire method.

Let’s see how we can get around this issue.

Optimized multithreaded lazy instantiation

To synchronize strategic points from the getHeroesDB() method, we need to create synchronized blocks within the method. Here’s an example:

public class ThreadSafeSynchronized { public static volatile HeroesDB heroesDB; public static HeroesDB getHeroesDB() { if(heroesDB == null) { synchronized (ThreadSafeSynchronized.class) { if(heroesDB == null) { heroesDB = new HeroesDB(); } } } return heroesDB; } static class HeroesDB { } }

In this code, we only synchronize the object creation if the instance is null. Otherwise, we will return the object instance.

Notice, also, that we synchronize the ThreadSafeSynchronized class, since we are using a static method. Then, we double-check to ensure the heroesDB instance is still null, since it’s possible that another thread might have instantiated it. Without double-checking, we could end up with more than one instance.

Another important point is that the variable heroesDB is volatile. This means that the variable’s value won’t be cached. This variable will always have the latest updated value when threads change it.

When to use eager instantiation

It’s better to use lazy instantiation for expensive objects that you might never use. However, if we are working with an object that we know will be used every time the application is started, and if the object’s creation is expensive, in terms of system resources, then it’s better to use eager instantiation.

Suppose we have to create a very expensive object such as a database connection, which we know we will always need. Waiting until this object is used could slow down the application. Eager instantiation makes more sense in this case.

A simple approach to eager instantiation

A simple way to implement eager instantiation is as follows:

public class HeroesDatabaseSimpleEager { public static final HeroesDB heroesDB = new HeroesDB(); static HeroesDB getHeroesDB() { return heroesDB; } static class HeroesDB { private HeroesDB() { System.out.println(“Instantiating heroesDB eagerly…”); } @Override public String toString() { return “HeroesDB instance”; } } public static void main(String[] args) { System.out.println(HeroesDatabaseSimpleEager.getHeroesDB()); } }

The output from this code would be:

Instantiating heroesDB eagerly… HeroesDB instance

Notice that in this case we don’t have the null check. HeroesDB is instantiated at the moment it’s declared as an instance variable within HeroesDatabaseSimpleEager. Therefore, every time we access the HeroesDatabaseSimpleEager class, we will get an instance from HeroesDB. We also overrode the toString() method to make the output of the HeroesDB instance simpler.

Now let’s see a more robust approach to eager instantiation, using an enum.

Eager instantiation with enum

Using an enum is a more robust way to create an eagerly instantiated object. Although the instance will only be created at the moment the enum is accessed, notice in the code below that we don’t have the null check for the object creation:

public enum HeroesDatabaseEnum { INSTANCE; int value; public int getValue() { return value; } public void setValue(int value) { this.value = value; } public static void main(String[] args) { System.out.println(HeroesDatabaseEnum.INSTANCE); } }

The output from this code would be:

Creating instance… INSTANCE

This code is thread-safe. It guarantees that we create only one instance and it serializes the object, meaning that we can more easily transfer it. Another detail is that with enums we have an implicit private constructor, which guarantees that we won’t create multiple instances unnecessarily. Enum is considered one of the best ways to use eager instantiation due to its simplicity and effectiveness.

Lazy instantiation vs. eager instantiation

Lazy instantiation is good when we know that we won’t always need to instantiate an object. Eager instantiation is better when we know we’ll always need to instantiate the object. Consider the pros and cons of each approach:

Lazy instantiation

Pros:

  • The object will be only instantiated if needed.

Cons:

  • It needs synchronization to work in a multithreaded environment.
  • Performance is slower due to the if check and synchronization.
  • There might be a significant delay in the application when the object is needed.

Eager instantiation

Pros:

  • In most cases, the object will be instantiated when the application is started.
  • There is no delay when using the object, since it will be already instantiated.
  • It works fine in a multithreaded environment.

Cons:

  • You might instantiate an object unnecessarily with this approach.

Lazy Homer beer creation challenge

In the following Java code challenge, you will see a lazy instantiation happening in a multithreaded environment.

Notice that we are using a ThreadPool. We could use the Thread class directly, but it’s preferable to use the Java concurrency API.

Based on what you’ve learned in this article, what do you think is most likely to happen when we run the following code?

import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class LazyHomerBeerCreationChallenge { public static int i = 0; public static Beer beer; static void createBeer() { if (beer == null) { try { Thread.sleep(200); beer = new Beer(); i++; } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) throws InterruptedException { ExecutorService executor = Executors.newFixedThreadPool(2); executor.submit(LazyHomerChallenge::createBeer); executor.submit(LazyHomerChallenge::createBeer); executor.awaitTermination(2, TimeUnit.SECONDS); executor.shutdown(); System.out.println(i); } public static class Beer {} }

Here are the options for this challenge. Please look carefully at the code and select one of them:

  1. A) 1
  2. B) 0
  3. C) 2
  4. D) An InterruptedException is thrown

What just happened? Lazy instantiation explained

The key concept of this code challenge is that there will be parallelism when two threads are accessing the same process. Therefore, since we have a Thread.sleep before the instantiation of beer, chances are that two instances of beer will be created.

There is a very small chance that the threads won’t run concurrently, depending on the JVM implementation. But there is a very high chance that we will end up with two instances due to the Thread.sleep method.

Now, looking at the code again, notice that we are using a thread pool to create the two threads, then we’re running the createBeer method with those threads.

Therefore, the correct answer to this challenge is: C, or the value of 2.

Conclusion

Lazy and eager instantiation are important concepts for optimizing performance with expensive objects. Here are some of the key points to remember about these design strategies:

  • Lazy instantiation needs a null check before the instantiation.
  • Synchronize objects for lazy instantiation in multithreaded environments.
  • Eager instantiation doesn’t require a null check for the object.
  • Using enum is an effective and simple approach for eager instantiation.

Copyright © 2022 IDG Communications, Inc.

Source

Leave a Reply

Your email address will not be published. Required fields are marked *