Mô hình thiết kế là cách tổng hợp các kỹ thuật lập trình phổ biến, giúp các lập trình viên dễ dàng trao đổi về vấn đề kỹ thuật. Trong bài viết trước, chúng ta đã đề cập đến việc sử dụng mô hình Decorator trong module I/O, điều này giúp dễ hình dung cấu trúc mã nguồn.
Hầu hết các mô hình thiết kế không trực tiếp cải thiện hiệu suất chương trình mà chỉ là cách tổ chức mã. Trong bài này, chúng ta sẽ phân tích một số mô hình liên quan đến hiệu suất như Proxy, Singleton, Flyweight và Prototype.
Tìm nguyên nhân chậm khi sử dụng Proxy động
Spring sử dụng rộng rãi mô hình Proxy thông qua CGLIB để tăng cường byte code. Trong các dự án phức tạp, có rất nhiều đoạn mã AOP như kiểm tra quyền, ghi log. Dưới đây là ví dụ sử dụng Arthas để xác định điểm nghẽn hiệu suất trong Proxy động.
@Component
public class SampleComponent {
public void execute() {
System.out.println("*******************");
}
}
@Aspect
@Component
public class LoggingAspect {
@Pointcut("execution(* com.example.demo.SampleComponent.*(..))")
public void logPointcut() {
}
@Before("logPointcut()")
public void beforeExecution() {
System.out.println("before");
try {
Thread.sleep(TimeUnit.SECONDS.toMillis(1));
} catch (InterruptedException e) {
throw new IllegalStateException();
}
}
}
@Controller
public class PerformanceTestController {
@Autowired
private SampleComponent component;
@ResponseBody
@GetMapping("/test")
public String test() {
long start = System.currentTimeMillis();
component.execute();
long duration = System.currentTimeMillis() - start;
String className = component.getClass().toString();
return className + " | " + duration;
}
}
Kết quả chạy chương trình cho thấy lớp được tạo ra là EnhancerBySpringCGLIB với thời gian thực thi khoảng 1023ms.
Mô hình Proxy
Mô hình Proxy cho phép kiểm soát truy cập đối tượng thông qua lớp đại diện. Java hỗ trợ hai cách tạo Proxy động:
- JDK Proxy: yêu cầu interface, sử dụng
InvocationHandlervàProxy - CGLIB: có thể proxy lớp cụ thể, sử dụng
MethodInterceptorvàEnhancer
Dưới đây là kết quả benchmark so sánh hiệu suất giữa hai cách:
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
Kết quả cho thấy JDK Proxy có hiệu suất tốt hơn CGLIB trong phiên bản Java 1.8.
Mô hình Singleton
Spring sử dụng thuộc tính scope để xác định phạm vi của bean. Khi sử dụng singleton (mặc định), chỉ có một phiên bản duy nhất được tạo trong container.
Cách tạo singleton an toàn cho luồng là sử dụng double-check với từ khóa volatile:
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Tuy nhiên, cách này hiện đã không còn được khuyến khích. Thay vào đó, nên sử dụng enum để đảm bảo tính an toàn và hiệu quả:
public class EnumSingleton {
private EnumSingleton() {
}
public static EnumSingleton getInstance() {
return Holder.INSTANCE;
}
private enum Holder {
INSTANCE;
private final EnumSingleton singleton;
Holder() {
singleton = new EnumSingleton();
}
}
}
Mô hình Flyweight
Mô hình này tối ưu hiệu suất bằng cách chia sẻ các đối tượng giống nhau. Ví dụ điển hình là quản lý các đối tượng Strategy:
Map<String, Strategy> strategies = new HashMap<>();
strategys.put("a", new StrategyA());
strategys.put("b", new StrategyB());
Đoạn mã trên có thể được coi là cả mô hình Flyweight (tái sử dụng đối tượng) và Strategy (tách biệt logic).
Mô hình Prototype
Mô hình này tạo đối tượng mới từ mẫu đã có. Trong Java, phương thức clone() của lớp Object là ví dụ tiêu biểu. Tuy nhiên, việc sử dụng clone() thường gặp khó khăn với đối tượng phức tạp:
public class Prototype implements Cloneable {
private String data;
public Prototype(String data) {
this.data = data;
}
@Override
public Prototype clone() {
return new Prototype(this.data);
}
}
Hiện nay, mô hình Prototype thường được sử dụng như một tư tưởng thiết kế hơn là công cụ tối ưu hiệu suất.