Sử dụng các mẫu thiết kế để tối ưu hiệu năng

Mẫu thiết kế là cách tổng hợp lại các kỹ thuật phát triển phần mềm phổ biến, giúp các lập trình viên có thể trao đổi ý tưởng một cách chuyên nghiệp và nhanh chóng hơn. Ví dụ, khi chúng ta đề cập đến việc sử dụng mẫu Decorator trong mô-đun I/O, bạn có thể dễ dàng hình dung được cách tổ chức mã nguồn của mô-đun này.

Thực tế là hầu hết các mẫu thiết kế không trực tiếp cải thiện hiệu suất chương trình mà chỉ cung cấp cách tổ chức mã nguồn tốt hơn. Trong bài viết này, chúng ta sẽ phân tích một số mẫu thiết kế liên quan đến hiệu suất, bao gồm Proxy, Singleton, Flyweight và Prototype.

Làm thế nào để tìm ra nguyên nhân gây chậm từ proxy động?

Spring sử dụng rộng rãi mẫu Proxy bằng cách tăng cường byte-code Java thông qua CGLIB. Trong các dự án phức tạp, có rất nhiều mã AOP (Aspect-Oriented Programming), chẳng hạn như kiểm tra quyền hoặc ghi log. Mặc dù giúp đơn giản hóa mã nguồn, nhưng AOP cũng có thể gây khó khăn cho những người không quen thuộc với mã nguồn.

Dưới đây là cách sử dụng Arthas để tìm nguyên nhân khiến proxy động hoạt động chậm. Cách này đặc biệt hữu ích trong các dự án phức tạp vì bạn không cần phải hiểu toàn bộ mã nguồn để xác định điểm nghẽn hiệu suất.

Trước tiên, hãy tạo một Bean đơn giản:

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

Tiếp theo, sử dụng @Aspect để tạo khía cạnh (aspect) và thêm lệnh sleep 1 giây vào phương thức trước:

@Aspect
@Component
public class MyAspect {
    @Pointcut("execution(* com.github.xjjdog.spring.SampleBean.*(..))")
    public void pointcut() {}

    @Before("pointcut()")
    public void before() {
        System.out.println("Executing before logic...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

Tạo một Controller để kiểm tra thời gian thực thi:

@Controller
public class PerformanceController {
    @Autowired
    private SampleBean sampleBean;

    @ResponseBody
    @GetMapping("/performance")
    public String testPerformance() {
        long startTime = System.currentTimeMillis();
        sampleBean.execute();
        long duration = System.currentTimeMillis() - startTime;
        return sampleBean.getClass().getName() + " | " + duration + "ms";
    }
}

Kết quả chạy chương trình cho thấy đối tượng Bean đã bị thay thế bởi lớp proxy của Spring (EnhancerBySpringCGLIB), và thời gian thực thi tăng lên đáng kể (~1023ms).

Sử dụng Arthas để phân tích chi tiết quá trình thực thi và tìm ra phương thức AOP tiêu tốn nhiều tài nguyên nhất. Sau khi khởi động Arthas, nhập lệnh sau để bắt đầu:

trace com.github.xjjdog.spring.SampleBean execute

Mẫu Proxy

Mẫu Proxy cho phép kiểm soát việc truy cập vào một đối tượng thông qua một lớp trung gian.

Java hỗ trợ hai cách để thực hiện proxy động: JDK và CGLib.

  • JDK: Phù hợp với interface, sử dụng các lớp InvocationHandlerProxy.
  • CGLib: Có thể proxy cả các lớp bình thường, sử dụng MethodInterceptorEnhancer.

Kết quả thử nghiệm hiệu suất giữa JDK và CGLib bằng JMH như sau:

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 với tốc độ tạo proxy:

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

Nhìn chung, trong phiên bản Java mới, sự khác biệt về tốc độ giữa JDK và CGLib không đáng kể. Spring chọn CGLib chủ yếu vì khả năng proxy các lớp bình thường.

Mẫu Singleton

Spring sử dụng annotation @Scope để xác định phạm vi của bean, mặc định là singleton. Khi một bean được định nghĩa là singleton, nó chỉ tồn tại duy nhất một instance trong suốt thời gian chạy ứng dụng.

Để đảm bảo tính đồng bộ trong môi trường đa luồng, mẫu Singleton thường được thực hiện bằng hai cách: Lazy InitializationEager Initialization.

Với Eager Initialization, đối tượng được khởi tạo ngay khi lớp được load:

private static final Singleton INSTANCE = new Singleton();

Double-check locking là cách phổ biến để đảm bảo Singleton vừa an toàn với đa luồng vừa hiệu quả:

private volatile static Singleton instance;

public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
            }
        }
    }
    return instance;
}

Một cách khác được khuyến nghị là sử dụng Enum:

public enum SingletonEnum {
    INSTANCE;

    public SingletonEnum getInstance() {
        return INSTANCE;
    }
}

Mẫu Flyweight

Mẫu Flyweight tập trung vào việc tối đa hóa việc tái sử dụng đối tượng bằng cách lưu trữ chúng trong một cấu trúc dữ liệu như HashMap. Đây là một mẫu thiết kế hiếm khi được áp dụng nhưng rất hiệu quả trong việc tối ưu hiệu suất.

Chẳng hạn, khi quản lý các chiến lược (strategy):

Map<String, Strategy> strategies = new HashMap<>();
strategies.put("A", new StrategyA());
strategies.put("B", new StrategyB());

Từ góc độ tái sử dụng đối tượng, đoạn mã trên thể hiện mẫu Flyweight; còn nếu xem xét về chức năng, đó lại là mẫu Strategy.

Mẫu Prototype

Mẫu Prototype cho phép sao chép một đối tượng hiện có để tạo ra một đối tượng mới. Trong Java, điều này thường được thực hiện bằng phương thức clone() của lớp Object.

Tuy nhiên, việc sử dụng clone() ít khi được khuyến khích do chỉ thực hiện shallow copy. Để tạo deep copy, có thể sử dụng các kỹ thuật như serialization hoặc chuyển đổi đối tượng thành JSON.

Do đó, mẫu Prototype ngày nay thường chỉ mang tính chất tư duy hơn là công cụ tăng tốc tạo đối tượng.

Thẻ: Spring AOP DesignPatterns

Đăng vào ngày 18 tháng 5 lúc 13:05