Giới thiệu về ThreadLocal trong Java

1. Khái quát chung

Trong bài viết này, chúng ta sẽ xem xét cấu trúc ThreadLocal từ gói java.lang . Điều này cung cấp cho chúng tôi khả năng lưu trữ dữ liệu riêng lẻ cho luồng hiện tại - và chỉ cần bọc nó trong một loại đối tượng đặc biệt.

2. API ThreadLocal

Các TheadLocal xây dựng cho phép chúng ta lưu trữ dữ liệu sẽ được truy cập chỉ bằng một thread cụ thể .

Giả sử rằng chúng ta muốn có một giá trị Integer sẽ được đóng gói với chuỗi cụ thể:

ThreadLocal threadLocalValue = new ThreadLocal();

Tiếp theo, khi chúng ta muốn sử dụng giá trị này từ một luồng, chúng ta chỉ cần gọi một phương thức get () hoặc set () . Nói một cách đơn giản, chúng ta có thể nghĩ rằng ThreadLocal lưu trữ dữ liệu bên trong bản đồ - với chủ đề là chìa khóa.

Do đó, khi chúng ta gọi một phương thức get () trên threadLocalValue , chúng ta sẽ nhận được giá trị Integer cho luồng yêu cầu:

threadLocalValue.set(1); Integer result = threadLocalValue.get();

Chúng ta có thể tạo một phiên bản của ThreadLocal bằng cách sử dụng phương thức tĩnh withInitial () và chuyển một nhà cung cấp cho nó:

ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 1);

Để xóa giá trị khỏi ThreadLocal , chúng ta có thể gọi phương thức remove () :

threadLocal.remove();

Để xem cách sử dụng ThreadLocal đúng cách, trước tiên, chúng ta sẽ xem xét một ví dụ không sử dụng ThreadLocal , sau đó chúng ta sẽ viết lại ví dụ của mình để tận dụng cấu trúc đó.

3. Lưu trữ Dữ liệu Người dùng trong Bản đồ

Hãy xem xét một chương trình cần lưu trữ dữ liệu Ngữ cảnh dành riêng cho người dùng trên mỗi id người dùng nhất định:

public class Context { private String userName; public Context(String userName) { this.userName = userName; } }

Chúng tôi muốn có một chủ đề cho mỗi id người dùng. Chúng ta sẽ tạo một lớp SharedMapWithUserContext triển khai giao diện Runnable . Việc triển khai trong phương thức run () gọi một số cơ sở dữ liệu thông qua lớp UserRepository trả về một đối tượng Context cho một userId nhất định .

Tiếp theo, chúng tôi lưu trữ ngữ cảnh đó trong Bản đồ ConcurentHashMap được khóa bởi userId :

public class SharedMapWithUserContext implements Runnable { public static Map userContextPerUserId = new ConcurrentHashMap(); private Integer userId; private UserRepository userRepository = new UserRepository(); @Override public void run() { String userName = userRepository.getUserNameForUserId(userId); userContextPerUserId.put(userId, new Context(userName)); } // standard constructor }

Chúng tôi có thể dễ dàng kiểm tra mã của mình bằng cách tạo và bắt đầu hai chuỗi cho hai userIds khác nhau và xác nhận rằng chúng tôi có hai mục nhập trong bản đồ userContextPerUserId :

SharedMapWithUserContext firstUser = new SharedMapWithUserContext(1); SharedMapWithUserContext secondUser = new SharedMapWithUserContext(2); new Thread(firstUser).start(); new Thread(secondUser).start(); assertEquals(SharedMapWithUserContext.userContextPerUserId.size(), 2);

4. Lưu trữ dữ liệu người dùng trong ThreadLocal

Chúng ta có thể viết lại ví dụ của mình để lưu trữ cá thể Ngữ cảnh người dùng bằng ThreadLocal . Mỗi luồng sẽ có cá thể ThreadLocal của riêng nó .

Khi sử dụng ThreadLocal , chúng ta cần hết sức cẩn thận vì mọi cá thể ThreadLocal đều được liên kết với một luồng cụ thể. Trong ví dụ của chúng tôi, chúng tôi có một luồng dành riêng cho từng userId cụ thể và luồng này do chúng tôi tạo nên chúng tôi có toàn quyền kiểm soát nó.

Phương thức run () sẽ tìm nạp ngữ cảnh người dùng và lưu trữ nó vào biến ThreadLocal bằng phương thức set () :

public class ThreadLocalWithUserContext implements Runnable { private static ThreadLocal userContext = new ThreadLocal(); private Integer userId; private UserRepository userRepository = new UserRepository(); @Override public void run() { String userName = userRepository.getUserNameForUserId(userId); userContext.set(new Context(userName)); System.out.println("thread context for given userId: " + userId + " is: " + userContext.get()); } // standard constructor }

Chúng tôi có thể kiểm tra nó bằng cách bắt đầu hai luồng sẽ thực thi hành động cho một userId nhất định :

ThreadLocalWithUserContext firstUser = new ThreadLocalWithUserContext(1); ThreadLocalWithUserContext secondUser = new ThreadLocalWithUserContext(2); new Thread(firstUser).start(); new Thread(secondUser).start();

Sau khi chạy mã này, chúng ta sẽ thấy trên đầu ra tiêu chuẩn mà ThreadLocal đã được đặt cho mỗi chủ đề nhất định:

thread context for given userId: 1 is: Context{userNameSecret='18a78f8e-24d2-4abf-91d6-79eaa198123f'} thread context for given userId: 2 is: Context{userNameSecret='e19f6a0a-253e-423e-8b2b-bca1f471ae5c'}

Chúng ta có thể thấy rằng mỗi người dùng có Ngữ cảnh riêng .

5. ThreadLocal s và Thread Pools

ThreadLocal cung cấp một API dễ sử dụng để giới hạn một số giá trị cho mỗi luồng. Đây là một cách hợp lý để đạt được an toàn luồng trong Java. Tuy nhiên, chúng ta nên hết sức cẩn thận khi chúng ta đang sử dụng các ThreadLocal và các nhóm luồng cùng nhau.

Để hiểu rõ hơn về cảnh báo có thể xảy ra, chúng ta hãy xem xét tình huống sau:

  1. Đầu tiên, ứng dụng mượn một luồng từ pool.
  2. Sau đó, nó lưu trữ một số giá trị giới hạn luồng vào ThreadLocal của luồng hiện tại .
  3. Khi quá trình thực thi hiện tại kết thúc, ứng dụng sẽ trả lại luồng đã mượn cho nhóm.
  4. Sau một thời gian, ứng dụng mượn cùng một luồng để xử lý một yêu cầu khác.
  5. Vì lần trước ứng dụng không thực hiện các thao tác dọn dẹp cần thiết, nên nó có thể sử dụng lại cùng một dữ liệu ThreadLocal cho yêu cầu mới.

Điều này có thể gây ra hậu quả đáng ngạc nhiên trong các ứng dụng đồng thời cao.

Một cách để giải quyết vấn đề này là xóa thủ công từng ThreadLocal sau khi chúng tôi sử dụng xong. Vì cách tiếp cận này cần xem xét mã nghiêm ngặt, nó có thể dễ xảy ra lỗi.

5.1. Mở rộng ThreadPoolExecutor

Hóa ra, có thể mở rộng lớp ThreadPoolExecutor và cung cấp triển khai hook tùy chỉnh cho các phương thức beforeExecute ()afterExecute () . Nhóm luồng sẽ gọi phương thức beforeExecute () trước khi chạy bất cứ thứ gì bằng luồng đã mượn. Mặt khác, nó sẽ gọi phương thức afterExecute () sau khi thực thi logic của chúng ta.

Do đó, chúng ta có thể mở rộng lớp ThreadPoolExecutor và xóa dữ liệu ThreadLocal trong phương thức afterExecute () :

public class ThreadLocalAwareThreadPool extends ThreadPoolExecutor { @Override protected void afterExecute(Runnable r, Throwable t) { // Call remove on each ThreadLocal } }

Nếu chúng tôi gửi yêu cầu của mình đến việc triển khai ExecutorService này , thì chúng tôi có thể chắc chắn rằng việc sử dụng ThreadLocal và các nhóm luồng sẽ không gây ra các mối nguy an toàn cho ứng dụng của chúng tôi.

6. Kết luận

Trong bài viết nhanh này, chúng ta đã xem xét cấu trúc ThreadLocal . Chúng tôi đã triển khai logic sử dụng ConcurrentHashMap được chia sẻ giữa các luồng để lưu trữ ngữ cảnh được liên kết với một userId cụ thể . Tiếp theo, chúng tôi viết lại ví dụ của mình để tận dụng ThreadLocal để lưu trữ dữ liệu được liên kết với một userId cụ thể và với một chuỗi cụ thể.

Bạn có thể tìm thấy việc triển khai tất cả các ví dụ và đoạn mã này trên GitHub.