11 Case Analysis How to Optimize Performance With Design Patterns

11 Case Analysis- How to Optimize Performance with Design Patterns #

The structure of code has a significant impact on the overall performance of an application. Well-structured code can avoid many potential performance problems and also plays a huge role in code scalability. Clear and hierarchical code also helps you identify bottlenecks in the system for targeted optimization.

Design patterns are summaries of commonly used development techniques, which provide programmers with a more professional and convenient way to communicate and solve problems. For example, as mentioned in “02 | Theoretical Analysis: Performance Optimization Can Follow Certain Rules, Talking about Common Entry Points,” the I/O module uses the decorator pattern, and you can easily understand the code organization of the I/O module.

In fact, most design patterns do not increase the performance of a program; they are just a way of organizing code. In this lesson, we will explain several design patterns related to performance, including the Proxy pattern, Singleton pattern, Flyweight pattern, Prototype pattern, etc.

How to Find the Cause of Slow Logic in Dynamic Proxies? #

Spring extensively utilizes the Proxy pattern, using CGLIB to enhance Java bytecodes. In complex projects, there can be a lot of AOP code, such as aspects for handling permissions and logging. While AOP makes coding more convenient, it also brings a lot of confusion for those who are not familiar with the project code.

Next, let’s analyze the specific reason for slow logic in dynamic proxies using arthas. This approach is highly effective in complex projects, as it enables you to locate performance bottlenecks without having to be familiar with the project code.

First, let’s create the simplest bean (see code in repository).

@Component 
public class ABean { 
    public void method() { 
        System.out.println("*******************"); 
    } 
} 

Then, we use the Aspect annotation to write the aspect, and in the @Before method, we make the thread sleep for 1 second.

@Aspect 
@Component 
public class MyAspect { 
    @Pointcut("execution(* com.github.xjjdog.spring.ABean.*(..)))") 
    public void pointcut() { 
    }  
    @Before("pointcut()") 
    public void before() { 
        System.out.println("before"); 
        try { 
            Thread.sleep(TimeUnit.SECONDS.toMillis(1)); 
        } catch (InterruptedException e) { 
            throw new IllegalStateException(); 
        } 
    } 
} 

Create a controller. When accessing the /aop link, the class name of the bean and its execution time will be output.

@Controller 
public class AopController { 
    @Autowired 
    private ABean aBean;  
    @ResponseBody 
    @GetMapping("/aop") 
    public String aop() { 
        long begin = System.currentTimeMillis(); 
        aBean.method(); 
        long cost = System.currentTimeMillis() - begin; 
        String cls = aBean.getClass().toString(); 
        return cls + " | " + cost; 
    } 
} 

The execution result is as follows. We can see that the AOP proxy has taken effect, and the Bean object in memory has become a subtype of EnhancerBySpringCGLIB, with the method call taking 1023ms.

class com.github.xjjdog.spring.ABean$$EnhancerBySpringCGLIB$$a5d91535 | 1023

Next, we use arthas to analyze this execution process and find the AOP method with the highest execution time. After starting arthas, we can see our application in the list, and here, we enter the analysis interface by typing 2.

15956792012866.jpg

Enter the trace command in the terminal and then access the /aop interface. The terminal will print some debug information, and we can find that the slow operation is the Spring proxy class.

trace com.github.xjjdog.spring.ABean method

15956796510862.jpg

Proxy Pattern #

The Proxy pattern allows us to control the access to an object through a proxy class.

There are two main ways to implement dynamic proxies in Java: one is using JDK (Java Development Kit), and the other is using CGLib (Code Generation Library).

  • The JDK way is interface-based, with the main related classes being InvocationHandler and Proxy.
  • CGLib can proxy ordinary classes, with the main related classes being MethodInterceptor and Enhancer.

This is a very commonly asked question in interviews. The complete code for both JDK and CGLib implementations is available in the repository, so I won’t include it here.

Here are the JMH (Java Microbenchmark Harness) test results for the speed of JDK and CGLib proxies:

Benchmark              Mode  Cnt      Score      Error   Units 
ProxyBenchmark.cglib  thrpt   10  78499.580 ± 1771.148  ops/ms 
ProxyBenchmark.jdk    thrpt   10  88948.858 ±  814.360  ops/ms 

I am currently using JDK version 1.8. As you can see, the speed of CGLib is not as fast as rumored (supposedly 10 times faster). In fact, its speed even slightly decreases compared to JDK. Let’s take a look at the creation speed of proxies, and the results are as follows. As you can see, JDK has double the throughput of CGLib when it comes to initializing proxy classes.

Benchmark                    Mode  Cnt      Score      Error   Units 
ProxyCreateBenchmark.cglib  thrpt   10   7281.487 ± 1339.779  ops/ms 
ProxyCreateBenchmark.jdk    thrpt   10  15612.467 ±  268.362  ops/ms 

In summary, there is not much difference in the creation and execution speed between JDK dynamic proxy and CGLib proxy in the new version of Java. Spring chooses CGLib mainly because it can proxy ordinary classes.

Singleton Pattern #

When creating components in Spring, you can specify their scope through the scope annotation to indicate whether the component is a prototype or a singleton.

When specified as a singleton (default behavior), there is only one instance of the component in the Spring container, and when you inject related components, you get the same instance.

If it is a regular singleton class, we usually make the constructor private. Singletons can be lazily loaded or eagerly loaded.

For those who understand the JVM class loading mechanism, it is known that a class goes through 5 steps from loading to initialization: loading, verification, preparation, resolution, and initialization.

2.png

Among them, static fields and static code blocks belong to the class and are executed during the initialization phase of class loading. They correspond to methods in bytecode and belong to the class (constructors). Since class initialization only occurs once, it ensures that this loading action is thread-safe.

Based on the above principle, as long as the initialization action of the singleton is placed in a method, the eager initialization can be achieved.

private static Singleton instace = new Singleton();  

Eager initialization is rarely used in code because it causes resource waste and generates many objects that may never be used. However, object initialization is different. Usually, when we create a new object, we call its constructor to initialize the object’s properties. Since multiple threads can call the function at the same time, we need to use the synchronized keyword to synchronize the generation process.

Currently, the most recognized singleton pattern that balances thread safety and efficiency is the double-check. Many interviewers will ask you to write it by hand and analyze its principle.

15957667011612.jpg

The above figure shows the key code of double-check. Let’s explain the four key points:

  • The first check. When instance is null, enter the object instantiation logic; otherwise, return directly.
  • Acquire the synchronization lock, which is the class lock.
  • The second check is the key. Without this null check, multiple threads may enter the synchronized block and generate multiple instances.
  • The last key point is the volatile keyword. In some older versions of Java, due to instruction reordering, the singleton may be created and not yet execute the constructor before it is used by other threads. This keyword can prevent bytecode instruction reordering, and it is common to add volatile when writing double-check code.

As you can see, the double-check approach is complicated and has many points to pay attention to. It is now considered an anti-pattern and is not recommended for use, and I also do not recommend using it in your own code. However, it can test the interviewee’s understanding of concurrency, so this question is often asked.

It is recommended to use the enum to implement lazy-loaded singleton, as shown in the code snippet below:

Effective Java also recommends this approach.

public class EnumSingleton { 
    private EnumSingleton() { 
    }   
public static EnumSingleton getInstance() { 
    return Holder.HOLDER.instance; 
} 
private enum Holder { 
    HOLDER; 
    private final EnumSingleton instance; 
    Holder() { 
        instance = new EnumSingleton(); 
    } 
} 
}

Flyweight Pattern #

Flyweight pattern is a rare design pattern that is specifically targeted at performance optimization. It maximizes object reuse by using shared technology. Flyweight pattern generally uses a unique identifier for identification and returns the corresponding object, making it very suitable to store in collections like HashMap.

The description above should be familiar to us, as we have seen the presence of flyweight pattern in some previous lessons, such as object pooling in lesson “09 | Case Study: Application Scenarios of Object Pooling” and object reuse in lesson “10 | Case Study: Goals and Notes on Object Reuse”.

Design patterns abstract our everyday coding behaviors and explain them from different perspectives. We can find some common points of design ideas when looking at different explanations of the same design pattern. For example, the singleton pattern is a specific case of the flyweight pattern, where object reuse is achieved by sharing a single instance.

It is worth mentioning that the same code can produce different effects depending on the interpretation. For example, consider the following code:

Map<String, Strategy> strategies = new HashMap<>();
strategies.put("a", new AStrategy());
strategies.put("b", new BStrategy());

If we consider it from the perspective of object reuse, it can be seen as an implementation of the flyweight pattern. If we consider it from the perspective of object functionality, it can be seen as the strategy pattern. Therefore, when discussing design patterns, we must pay attention to the context and differences in language.

Prototype Pattern #

The prototype pattern is similar to the idea of copy and paste. It involves creating an instance first and then creating new objects through this instance. In Java, the most typical example is the clone() method of the Object class.

However, this method is rarely used in coding. The prototype mentioned in the previous lesson about the proxy pattern is not implemented through cloning but through more complex reflection techniques.

One important reason is that clone() only copies objects at the current level, resulting in a shallow copy. In reality, objects are often very complex, and achieving deep copy through clone() requires a lot of coding in the clone() method, making it much less convenient than using the new keyword.

To achieve deep copy, other means such as serialization can be used, such as implementing the Serializable interface or converting objects to JSON.

Therefore, in practice, the prototype pattern has become a concept rather than a tool to speed up object creation.

Summary #

In this lesson, we mainly looked at several design patterns related to performance, including some high-frequency exam topics. We learned about the two ways to implement dynamic proxy in Java and their differences. In the current version of the JVM, there is not much difference in performance between them. We also learned about the three ways to create singletons and saw an example of a double-check failure. In general coding, it is recommended to use enums to implement singletons. Lastly, we learned about the flyweight pattern and prototype pattern, which are more conceptual and do not have fixed coding patterns.

We also briefly learned how to use arthas’ “trace” command to find time-consuming code blocks, and we ultimately identified the issue in Spring’s AOP module. This kind of scenario frequently occurs in complex projects, so special attention is required.

In addition, in the realm of design patterns, the producer-consumer pattern is the most beneficial for performance, such as asynchronous messaging, the reactor model, etc. We will discuss this in detail in the next lesson, “15 | Case Study: From BIO to NIO, and to AIO”.

However, to understand this content, you need to have some knowledge of multithreaded programming. Therefore, in the next lesson, I will explain some key points of parallel computing. Make sure to join the class on time.