ThreadLocal là một lớp tiện ích khá thông dụng trong gói java.lang của JDK, cung cấp chức năng ngữ cảnh luồng. Ứng dụng phổ biến nhất có lẽ là dùng ThreadLocal để lưu những thông tin quan trọng như ngữ cảnh người dùng sau khi đăng nhập.
Ngữ cảnh luồng còn có thể giải quyết một cách gọn gàng nhiều vấn đề tưởng chừng khá phức tạp, chẳng hạn: vấn đề tích hợp các framework spring-mybatis:
Khi phát triển dựa trên hệ sinh thái spring, chúng ta thường giao việc quản lý giao dịch cho spring, dùng chú thích transactional để cấu hình việc mở giao dịch cũng như các thông tin như mức cô lập giao dịch. Chúng ta cũng không thao tác cơ sở dữ liệu trực tiếp qua jdbc mà truy cập thông qua framework mybatis. spring và mybatis là hai framework hoàn toàn độc lập, nhưng khi sử dụng, dường như mybatis có thể lấy và dùng ngay connection cơ sở dữ liệu do spring mở. Về cơ bản, chúng ta chẳng cần làm gì thêm, chỉ cần thêm gói jar mybatis-spring-boot-stater là được.
Điểm mấu chốt để hiện thực hóa thiết kế tinh tế này chính là ThreadLocal, nguyên lý khái quát là gói jar stater cuối cùng sẽ giúp chúng ta đưa vào một MybatisAutoConfiguration và thông qua SPI đưa vào container bean của dịch vụ chúng ta, tại MybatisAutoConfiguration đã được cấu hình trong mybatis cần thiết SqlSessionFactory,đồng thời đã tiêm vào thành phần riêng của spring là SpringManagedTransactionFactory(đã triển khai TransactionFactory,nhà máy giao dịch là cơ chế mở rộng do mybatis cung cấp, cho phép ủy thác việc quản lý giao dịch ra bên ngoài).
Toàn bộ quy trình thực thi là phương thức có chú thích transactional cuối cùng sẽ được AOP tạo proxy; lớp proxy sẽ lấy một connection từ dataSource trong thông báo trước, rồi đưa vào một ThreadLocal (thực ra bên trong ThreadLocal là một map, key là dataSource, value là connectionHolder tương đương với connection), sau đó khi mybatis thực thi sql sẽ trước tiên thông qua TransactionFactory Lấy connection, vì đối tượng được tiêm vào lúc này TransactionFactory Có SpringManagedTransactionFactory,vì vậy sẽ lấy connection của dataSource tương ứng từ ThreadLocal.
Giới thiệu ngắn gọn nguyên lý ThreadLocal:
ThreadLocalMap chính là một bảng băm dùng phương pháp dò tuyến tính để giải quyết xung đột, mỗi luồng đều tạo một bản, trong mỗi đối tượng ThreadLocal đều có một threadLocalHashCode Dùng để tính khóa băm.
So sánh với HashMap,ThreadLocalMap Việc chọn thuật toán xử lý xung đột bằng dò tuyến tính thay vì phương pháp nối chuỗi có thể dựa trên những cân nhắc sau:
1、Cân nhắc về bộ nhớ, do tình huống sử dụng khác nhau, lượng dữ liệu của ThreadLocalMap thường nhỏ hơn HashMap rất nhiều. Khi lượng dữ liệu ít, phương pháp nối chuỗi sẽ tiêu tốn thêm khá nhiều không gian cho các con trỏ
2、Hiệu suất, trên cơ sở 1, khi số lượng nhỏ, phần không gian tiết kiệm được có thể dùng để tạo một bảng băm có hệ số tải thấp hơn, giảm xung đột và nâng cao tốc độ tra cứu trung bình

Vấn đề
A Trong các tác vụ dựa trên nhóm luồng, tồn tại vấn đề truyền ngữ cảnh, đặc biệt là trong mô hình lập trình web dựa trên Tomcat. Do bản thân Tomcat sử dụng nhóm luồng để xử lý tương tác yêu cầu-phản hồi giữa máy chủ và máy khách, mỗi ứng dụng web đều phải tự xử lý việc truyền ngữ cảnh luồng cho chính xác.
B Vấn đề hiệu suất, nếu ứng dụng có quá nhiều ThreadLocal, sau khi bảng băm phình to, tốc độ tra cứu sẽ giảm
Với vấn đề A, cách giải quyết phổ biến khá thống nhất, tôi đăng trực tiếp mã trong hệ thống của chúng tôi ở đây
Phương án này có hai vấn đề:
1、Nếu sau này có thêm ThreadLocal cần truyền đúng cách, phải sửa mã để thêm ThreadLocal mới vào logic wrap; chỉ cần bỏ sót là sẽ phát sinh bug
2、Trong một số trường hợp, tác vụ có thể được thực thi bởi chính luồng gửi tác vụ, ví dụ:
Khi nhóm luồng đã đầy và phần của nhóm luồng
RejectedExecutionHandlersử dụngCallerRunsPolicykhi đó, tác vụ được gửi vào nhóm luồng sẽ được thực thi trực tiếp trong luồng gửi tác vụ, lúc này logic dọn dẹp cuối cùng sẽ khiến ngữ cảnh của luồng gửi tác vụ bị mấtTương tự, sử dụng
ForkJoinPool(bao gồm thực thi song songStreamvàCompletableFuture,ở tầng bên dưới sử dụngForkJoinPool)trong các tình huống đó, phần được mở rộngForkJoinTasksẽ được thực thi trực tiếp trong luồng gửi tác vụ. Việc dọn dẹp cuối cùng cũng sẽ khiến ngữ cảnh của luồng gửi bị mất
Giải pháp:
Vấn đề 1: Nếu lớp ThreadLocal hỗ trợ một API có thể lấy được tổng số đối tượng ThreadLocal hiện có trong ứng dụng thì vấn đề sẽ được giải quyết dễ dàng, tiếc là không hỗ trợ. Nếu cố làm, có thể dùng cơ chế phản chiếu để lấy phần nằm trong đối tượng thread inheritableThreadLocals(chính là một ThreadLocalMap)thuộc tính, sau đó duyệt bảng băm để lấy toàn bộ entry là có thể thu được tất cả các cặp khóa–giá trị threadlocal. Một hướng khác là bọc thêm một lớp cho ThreadLocal của jdk để bổ sung các chức năng nêu trên.
Vấn đề 2:Có thể giải quyết bằng cách hoàn thiện logic của wrap:khi luồng bắt đầu chạy, trước tiên lấy và lưu tạm ngữ cảnh của luồng hiện tại, sau đó thay ngữ cảnh của luồng này bằng ngữ cảnh của luồng gửi tác vụ. Trước khi luồng kết thúc, không xóa ngữ cảnh mà dùng dữ liệu đã lưu tạm để khôi phục ngữ cảnh của luồng này.
transmittable-thread-local
Đã có sẵn công cụ rồi, nguyên lý triển khai chính là bắt đầu từ hai hướng trên, đồng thời quy trình này được đặt một cái tên CRR(capture/replay/restore) chế độ。
Có ba cách sử dụng:
1 ### Bổ nghĩa Runnable và Callable
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父线程中设置
context.set("value-set-in-parent");
Runnable task = new RunnableTask();
// 额外的处理,生成修饰了的对象ttl
Runnable Runnable ttlRunnable = TtlRunnable.get(task);
executorService.submit(ttlRunnable);
// =====================================================
// Task中可以读取,值是"value-set-in-parent"
String value = context.get();2 Bọc bổ sung chức năng cho nhóm luồng
ExecutorService executorService = ...
// 额外的处理,生成修饰了的对象
executorService executorService = TtlExecutors.getTtlExecutorService(executorService);
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父线程中设置
context.set("value-set-in-parent");
Runnable task = new RunnableTask();
Callable call = new CallableTask();
executorService.submit(task);
executorService.submit(call);
// =====================================================
// Task或是Call中可以读取,值是"value-set-in-parent"
String value = context.get();3 ### Sử dụng Java Agent để bổ nghĩa cho JDK Lớp triển khai nhóm luồng
// ## 1. 框架上层逻辑,后续流程框架调用业务 ##
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
context.set("value-set-in-parent");
// ## 2. 应用逻辑,后续流程业务调用框架下层逻辑 ##
ExecutorService executorService = Executors.newFixedThreadPool(3);
Runnable task = new RunnableTask();
Callable call = new CallableTask();
executorService.submit(task);
executorService.submit(call);
// ## 3. 框架下层逻辑 ##
// Task或是Call中可以读取,值是"value-set-in-parent"
String value = context.get();Ưu điểm của việc sử dụng java agent là không cần can thiệp vào mã ứng dụng; còn về nhược điểm, tôi thấy vấn đề lớn nhất là nếu quên thêm tham số khi khởi động jvm thì có thể xảy ra sự cố.
Xem sơ qua logic mã nguồn
Đối với vấn đề B, thực ra khi phát triển ứng dụng, người ta thường không cân nhắc vấn đề này vì yêu cầu về hiệu quả thời gian không quá cao. Nhưng trong quá trình phát triển một số phần mềm trung gian ở tầng thấp, việc tối ưu hiệu năng có thể được chú trọng hơn. Chẳng hạn, netty đã chọn tự triển khai FastThreadLocal thay thế ThreadLocal của jdk, đồng thời cung cấp bộ đi kèm FastThreadLocalThread,tại FastThreadLocalThread và FastThreadLocal Khi dùng kết hợp, có thể luôn bảo đảm độ phức tạp thời gian tra cứu O(1), nguyên lý của giải pháp này là đổi không gian lấy thời gian. Để tránh hiệu suất truy vấn giảm sau khi bảng băm xảy ra xung đột băm, một mảng đủ dài sẽ được tạo ra, mỗi FastThreadLocal Khi khởi tạo, mỗi đối tượng đều được cấp một chỉ số mảng duy nhất, tương đương mỗi người một chỗ; khi truy vấn, chỉ cần dùng index của mình để lấy trực tiếp từ mảng.
Đăng nhanh một đoạn mã nguồn