1. Gambaran keseluruhan
Dalam artikel ini, kita akan melihat konstruk ThreadLocal dari pakej java.lang . Ini memberi kita kemampuan untuk menyimpan data secara berasingan untuk utas semasa - dan hanya membungkusnya dalam jenis objek khas.
2. ThreadLocal API
The TheadLocal konstruk membolehkan kita untuk menyimpan data yang akan hanya boleh diakses oleh benang tertentu .
Katakan bahawa kita ingin mempunyai nilai Integer yang akan digabungkan dengan utas tertentu:
ThreadLocal threadLocalValue = new ThreadLocal();
Seterusnya, apabila kita ingin menggunakan nilai ini dari utas, kita hanya perlu memanggil kaedah get () atau set () . Ringkasnya, kita dapat berfikir bahawa ThreadLocal menyimpan data di dalam peta - dengan utas sebagai kunci.
Oleh kerana itu, apabila kita memanggil kaedah get () pada threadLocalValue , kita akan mendapat nilai Integer untuk thread yang meminta:
threadLocalValue.set(1); Integer result = threadLocalValue.get();
Kita boleh membina contoh ThreadLocal dengan menggunakan kaedah statik withInitial () dan memberikan pembekal kepadanya:
ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 1);
Untuk membuang nilai dari ThreadLocal , kita boleh memanggil kaedah remove () :
threadLocal.remove();
Untuk melihat bagaimana menggunakan ThreadLocal dengan betul, pertama, kita akan melihat contoh yang tidak menggunakan ThreadLocal , maka kita akan menulis semula contoh kita untuk memanfaatkan konstruk tersebut.
3. Menyimpan Data Pengguna dalam Peta
Mari pertimbangkan program yang perlu menyimpan data Konteks khusus pengguna bagi setiap id pengguna yang diberikan:
public class Context { private String userName; public Context(String userName) { this.userName = userName; } }
Kami mahu mempunyai satu utas setiap id pengguna. Kami akan membuat kelas SharedMapWithUserContext yang menerapkan antara muka Runnable . Pelaksanaan dalam metode run () memanggil beberapa pangkalan data melalui kelas UserRepository yang mengembalikan objek Konteks untuk userId tertentu .
Seterusnya, kami menyimpan konteks itu dalam ConcurentHashMap yang diketik 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 }
Kami dapat menguji kod kami dengan mudah dengan membuat dan memulakan dua utas untuk dua ID pengguna yang berbeza dan menegaskan bahawa kami mempunyai dua entri dalam 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 boleh menulis semula contoh kita untuk menyimpan contoh Konteks pengguna menggunakan ThreadLocal . Setiap utas akan mempunyai contoh ThreadLocal sendiri .
Semasa menggunakan ThreadLocal , kita perlu sangat berhati-hati kerana setiap contoh ThreadLocal dikaitkan dengan utas tertentu. Dalam contoh kami, kami mempunyai utas khusus untuk setiap ID pengguna tertentu , dan utas ini dibuat oleh kami, jadi kami mempunyai kawalan penuh ke atasnya.
Kaedah run () akan mengambil konteks pengguna dan menyimpannya ke dalam pemboleh ubah ThreadLocal menggunakan kaedah 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 boleh mengujinya dengan memulakan dua utas yang akan melaksanakan tindakan untuk pengguna ID tertentu :
ThreadLocalWithUserContext firstUser = new ThreadLocalWithUserContext(1); ThreadLocalWithUserContext secondUser = new ThreadLocalWithUserContext(2); new Thread(firstUser).start(); new Thread(secondUser).start();
Setelah menjalankan kod ini, kita akan melihat pada output standard yang ditetapkan oleh ThreadLocal untuk setiap 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 bahawa setiap pengguna mempunyai Konteksnya sendiri .
5. Kolam ThreadLocal dan Thread
ThreadLocal menyediakan API yang mudah digunakan untuk membatasi beberapa nilai pada setiap utas . Ini adalah kaedah yang wajar untuk mencapai keselamatan benang di Jawa. Walau bagaimanapun, kita harus berhati-hati ketika menggunakan ThreadLocal dan kumpulan benang bersama.
Untuk lebih memahami kemungkinan peringatan, mari kita pertimbangkan senario berikut:
- Pertama, aplikasi meminjam benang dari kolam.
- Kemudian menyimpan beberapa nilai terhad benang ke dalam ThreadLocal thread semasa .
- Setelah pelaksanaan semasa selesai, aplikasi mengembalikan utas yang dipinjam ke kumpulan.
- Selepas beberapa ketika, aplikasi meminjam urutan yang sama untuk memproses permintaan lain.
- Oleh kerana aplikasi tidak melakukan pembersihan yang diperlukan terakhir kali, aplikasi tersebut mungkin menggunakan kembali data ThreadLocal yang sama untuk permintaan baru.
Ini boleh menyebabkan akibat yang mengejutkan dalam aplikasi yang serentak.
Salah satu cara untuk menyelesaikan masalah ini ialah membuang setiap ThreadLocal secara manual setelah kami selesai menggunakannya. Oleh kerana pendekatan ini memerlukan tinjauan kod yang ketat, rawan ralat boleh dilakukan.
5.1. Memperluas ThreadPoolExecutor
Ternyata, adalah mungkin untuk memperluas kelas ThreadPoolExecutor dan menyediakan pelaksanaan hook khusus untuk kaedah sebelumExecute () dan afterExecute () . Kumpulan utas akan memanggil kaedah sebelumExecute () sebelum menjalankan apa-apa menggunakan utas yang dipinjam. Sebaliknya, ia akan memanggil kaedah afterExecute () setelah melaksanakan logik kami.
Oleh itu, kita dapat memperluas kelas ThreadPoolExecutor dan membuang data ThreadLocal dalam kaedah afterExecute () :
public class ThreadLocalAwareThreadPool extends ThreadPoolExecutor { @Override protected void afterExecute(Runnable r, Throwable t) { // Call remove on each ThreadLocal } }
Sekiranya kami mengemukakan permintaan kami untuk pelaksanaan ExecutorService ini , maka kami dapat memastikan bahawa menggunakan ThreadLocal dan kumpulan kolam tidak akan menimbulkan bahaya keselamatan untuk aplikasi kami.
6. Kesimpulannya
Dalam artikel ringkas ini, kami melihat konstruk ThreadLocal . Kami menerapkan logik yang menggunakan ConcurrentHashMap yang dikongsi antara utas untuk menyimpan konteks yang berkaitan dengan userId tertentu . Seterusnya, kami menulis semula contoh kami untuk memanfaatkan ThreadLocal untuk menyimpan data yang dikaitkan dengan ID pengguna tertentu dan dengan utas tertentu.
Pelaksanaan semua contoh dan coretan kod ini boleh didapati di GitHub.