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
InvocationHandlervàProxy. - CGLib: Có thể proxy cả các lớp bình thường, sử dụng
MethodInterceptorvàEnhancer.
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 Initialization và Eager 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.