Pengantar ThreadLocal di Java

1. Ikhtisar

Pada artikel ini, kita akan melihat konstruksi ThreadLocal dari paket java.lang . Ini memberi kita kemampuan untuk menyimpan data secara individual untuk utas saat ini - dan cukup membungkusnya dalam tipe objek khusus.

2. API ThreadLocal

The TheadLocal membangun memungkinkan kita untuk menyimpan data yang akan diakses hanya oleh thread tertentu .

Katakanlah kita ingin memiliki nilai Integer yang akan digabungkan dengan utas tertentu:

ThreadLocal threadLocalValue = new ThreadLocal();

Selanjutnya, ketika kita ingin menggunakan nilai ini dari sebuah thread, kita hanya perlu memanggil metode get () atau set () . Sederhananya, kita dapat berpikir bahwa ThreadLocal menyimpan data di dalam peta - dengan utas sebagai kuncinya.

Karena fakta itu, ketika kita memanggil metode get () pada threadLocalValue , kita akan mendapatkan nilai Integer untuk utas yang meminta:

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

Kita bisa membangun sebuah instance dari ThreadLocal dengan menggunakan metode statis withInitial () dan mengirimkan pemasok ke sana:

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

Untuk menghapus nilai dari ThreadLocal , kita dapat memanggil metode remove () :

threadLocal.remove();

Untuk melihat bagaimana menggunakan ThreadLocal dengan benar, pertama, kita akan melihat contoh yang tidak menggunakan ThreadLocal , kemudian kita akan menulis ulang contoh kita untuk memanfaatkan konstruksi itu.

3. Menyimpan Data Pengguna di Peta

Mari pertimbangkan program yang perlu menyimpan data Konteks khusus pengguna per id pengguna tertentu:

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

Kami ingin memiliki satu utas per id pengguna. Kami akan membuat kelas SharedMapWithUserContext yang mengimplementasikan antarmuka Runnable . Implementasi dalam metode run () memanggil beberapa database melalui kelas UserRepository yang mengembalikan objek Konteks untuk userId tertentu .

Selanjutnya, kami menyimpan konteks tersebut di ConcurentHashMap yang dikunci oleh 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 }

Kita dapat dengan mudah menguji kode kita dengan membuat dan memulai dua utas untuk dua userIds yang berbeda dan menegaskan bahwa kita memiliki dua entri di peta 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. Menyimpan Data Pengguna di ThreadLocal

Kita dapat menulis ulang contoh kita untuk menyimpan contoh Konteks pengguna menggunakan ThreadLocal . Setiap utas akan memiliki instance ThreadLocal sendiri .

Saat menggunakan ThreadLocal , kita harus sangat berhati-hati karena setiap instance ThreadLocal dikaitkan dengan thread tertentu. Dalam contoh kami, kami memiliki utas khusus untuk setiap userId tertentu , dan utas ini dibuat oleh kami, jadi kami memiliki kendali penuh atasnya.

Metode run () akan mengambil konteks pengguna dan menyimpannya ke dalam variabel ThreadLocal menggunakan metode 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 }

Kita dapat mengujinya dengan memulai dua utas yang akan menjalankan tindakan untuk userId tertentu :

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

Setelah menjalankan kode ini, kita akan melihat pada output standar yang ditetapkan ThreadLocal per utas yang diberikan:

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'}

Kita dapat melihat bahwa setiap pengguna memiliki Konteksnya sendiri .

5. ThreadLocal s dan Thread Pools

ThreadLocal menyediakan API yang mudah digunakan untuk membatasi beberapa nilai ke setiap utas. Ini adalah cara yang masuk akal untuk mencapai keamanan thread di Java. Namun, kita harus ekstra hati-hati saat menggunakan ThreadLocal dan kumpulan thread bersama-sama.

Untuk lebih memahami kemungkinan peringatan, mari pertimbangkan skenario berikut:

  1. Pertama, aplikasi meminjam utas dari kolam.
  2. Kemudian ia menyimpan beberapa nilai yang dibatasi thread ke dalam ThreadLocal thread saat ini .
  3. Setelah eksekusi saat ini selesai, aplikasi mengembalikan utas yang dipinjam ke kumpulan.
  4. Setelah beberapa saat, aplikasi meminjam utas yang sama untuk memproses permintaan lain.
  5. Karena aplikasi tidak melakukan pembersihan yang diperlukan terakhir kali, aplikasi dapat menggunakan kembali data ThreadLocal yang sama untuk permintaan baru.

Ini dapat menyebabkan konsekuensi yang mengejutkan dalam aplikasi yang sangat berbarengan.

Salah satu cara untuk mengatasi masalah ini adalah dengan menghapus setiap ThreadLocal secara manual setelah kami selesai menggunakannya. Karena pendekatan ini membutuhkan peninjauan kode yang ketat, pendekatan ini rentan terhadap kesalahan.

5.1. Memperluas ThreadPoolExecutor

Ternyata, kelas ThreadPoolExecutor dapat diperluas dan menyediakan implementasi hook kustom untuk metode beforeExecute () dan afterExecute () . Kumpulan thread akan memanggil metode beforeExecute () sebelum menjalankan apa pun menggunakan thread yang dipinjam. Di sisi lain, itu akan memanggil metode afterExecute () setelah menjalankan logika kita.

Oleh karena itu, kita dapat memperluas kelas ThreadPoolExecutor dan menghapus data ThreadLocal dalam metode afterExecute () :

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

Jika kami mengirimkan permintaan kami untuk implementasi ExecutorService ini , maka kami dapat yakin bahwa menggunakan ThreadLocal dan kumpulan thread tidak akan menimbulkan bahaya keamanan untuk aplikasi kami.

6. Kesimpulan

Dalam artikel singkat ini, kami melihat konstruksi ThreadLocal . Kami menerapkan logika yang menggunakan ConcurrentHashMap yang dibagikan di antara utas untuk menyimpan konteks yang terkait dengan userId tertentu . Selanjutnya, kami menulis ulang contoh kami untuk memanfaatkan ThreadLocal untuk menyimpan data yang terkait dengan userId tertentu dan dengan utas tertentu.

Penerapan semua contoh dan cuplikan kode ini dapat ditemukan di GitHub.