آشنایی با ThreadLocal در جاوا — راهنمای جامع
در این مقاله به بررسی سازه ThreadLocal در جاوا میپردازیم. این سازه در پکیج java.lang package قرار دارد و به ما امکان ذخیرهسازی تکتک دادهها را در نخ جاری میدهد. بدین ترتیب میتوان دادهها را به سادگی درون یک نوع خاصی از شیء قرار داد.
ThreadLocal API
سازه ThreadLocal به ما این امکان را میدهد که دادههایی که تنها از سوی یک نخ خاص قابل دسترسی هستند را ذخیره کنیم.
فرض کنید میخواهیم یک مقدار صحیح داشته باشیم که درون نخ خاصی بستهبندی شود:
1ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();
سپس زمانی که از این مقدار از درون یک نخ استفاده میکنیم، تنها باید یک متد ()get یا ()set را فراخوانی کنیم. به بیان ساده میتوان تصور کرد که ThreadLocal دادهها را درون یک map ذخیره میکند که کلید آن خود نخ است. به دلیل این واقعیت زمانی که یک متد ()get را روی threadLocalValue فرا میخوانیم یک مقدار صحیح برای نخ مورد تقاضا دریافت میکنیم:
1threadLocalValue.set(1);
2Integer result = threadLocalValue.get();
با استفاده از متد استاتیک ()withInitial میتوان یک وهله از ThreadLocal ساخته و یک supplier به آن ارسال کرد:
1ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
برای حذف مقدار از ThreadLocal میتوانیم یک متد ()remove را فراخوانی کنیم:
1threadLocal.remove();
برای این که با شیوه استفاده صحیح از ThreadLocal آشنا شوییم، ابتدا به بررسی مثالی میپردازیم که یک ThreadLocal ندارد و سپس آن را با بهرهگیری از ThreadLocal بازنویسی میکنیم.
ذخیره دادههای کاربر در یک map
برنامهای را تصور کنید که باید دادههای Context خاص کاربر را بسته به id کاربر مفروض ذخیره کند:
1public class Context {
2 private String userName;
3
4 public Context(String userName) {
5 this.userName = userName;
6 }
7}
ما باید یک نخ برای هر id داشته باشیم. به این منظور یک کلاس SharedMapWithUserContext ایجاد میکنیم که اینترفیس Runnable را پیادهسازی میکند. این پیادهسازی در متد ()run نوعی فراخوانی به پایگاه داده از طریق کلاس UserRepository دارد و یک شیء Context برای userId مفروض بازگشت میدهد. سپس آن context را در ConcurentHashMap با کلید userId ذخیره میکنیم:
1public class SharedMapWithUserContext implements Runnable {
2
3 public static Map<Integer, Context> userContextPerUserId
4 = new ConcurrentHashMap<>();
5 private Integer userId;
6 private UserRepository userRepository = new UserRepository();
7
8 @Override
9 public void run() {
10 String userName = userRepository.getUserNameForUserId(userId);
11 userContextPerUserId.put(userId, new Context(userName));
12 }
13
14 // standard constructor
15}
ما به سادگی میتوانیم کد را با ایجاد و آغاز دو نخ برای دو userId متفاوت تست کنیم و مطمئن شویم که دو مدخل در map به نام userContextPerUserId داریم:
1SharedMapWithUserContext firstUser = new SharedMapWithUserContext(1);
2SharedMapWithUserContext secondUser = new SharedMapWithUserContext(2);
3new Thread(firstUser).start();
4new Thread(secondUser).start();
5
6assertEquals(SharedMapWithUserContext.userContextPerUserId.size(), 2);
ذخیره دادههای کاربر در ThreadLocal
مثال قبلی ذخیرهسازی وهله Context کاربر را میتوانیم با استفاده از یک ThreadLocal بازنویسی کنیم. هر نخ دارای یک وهله خاص از ThreadLocal است. زمانی که از ThreadLocal استفاده میکنیم باید بسیار مراقب باشیم، زیرا هر وهله ThreadLocal با یک نخ خاص مرتبط است. در مثال مورد بررسی یک نخ اختصاصی برای userId خاص داریم و این نخ از سوی ما ایجاد شده است، از این رو کنترل کاملی روی آن داریم. متد ()run اقدام به واکشی context کاربر کرده و آن را با استفاده از متد ()set در متغیر ThreadLocal ذخیره میکند:
1public class ThreadLocalWithUserContext implements Runnable {
2
3 private static ThreadLocal<Context> userContext
4 = new ThreadLocal<>();
5 private Integer userId;
6 private UserRepository userRepository = new UserRepository();
7
8 @Override
9 public void run() {
10 String userName = userRepository.getUserNameForUserId(userId);
11 userContext.set(new Context(userName));
12 System.out.println("thread context for given userId: "
13 + userId + " is: " + userContext.get());
14 }
15
16 // standard constructor
17}
با آغاز کردن دو نخ که اکشن منورد نظر را برای userId مفروض اجرا میکنند، میتوانیم کد فوق را تست کنیم:
1ThreadLocalWithUserContext firstUser
2 = new ThreadLocalWithUserContext(1);
3ThreadLocalWithUserContext secondUser
4 = new ThreadLocalWithUserContext(2);
5new Thread(firstUser).start();
6new Thread(secondUser).start();
پس از اجرای کد فوق، در خروجی استاندارد میبینیم که ThreadLocal برای نخ مفروض تعیین شده است:
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'}
چنان که میبینید هر کدام از کاربران Context خاص خود را دارند.
از ThreadLocal به همراه ExecutorService استفاده نکنید
اگر بخواهیم از ExecutorService استفاده کنیم و یک Runnable به آن تحویل بدهیم، استفاده از ThreadLocal نتایج غیر قطعی به ما ارائه میکند، زیرا تضمینی نداریم که هر اکشن Runnable برای یک userId مفروض، هر بار از سوی نخ یکسانی که آن را اجرا کرده است، مدیریت شود.
به همین جهت، ThreadLocal ما میان userId-های مختلف به اشتراک گذاشته میشود. به همین دلیل است که نباید از TheadLocal به همراه ExecutorService استفاده کنیم. TheadLocal تنها باید زمانی استفاده شود که کنترل کاملی داشته باشیم که کدام نخ کدام اکشن runnable را برای اجرا انتخاب میکند.
سخن پایانی درباره ThreadLocal در جاوا
در این مقاله به بررسی سازه ThreadLocal پرداختیم. منطقی را پیادهسازی کردیم که از ConcurrentHashMap مشترک بین نخها برای ذخیرهسازی context مرتبط با یک userId خاص استفاده میکند. سپس مثال خود را بهرهگیری از ThreadLocal برای ذخیرهسازی دادههایی که با userId خاص و با نخ خاص مرتبط هستند، بازنویسی کردیم. همه کدهای مطرحشده در این مقاله را میتوانید در این ریپوی گیتهاب (+) ملاحظه کنید. این یک پروژه Maven است و لذا میتوانید به سادگی در پروژه خود ایمپورت کرده و مورد استفاده قرار دهید.
اگر این مطالب برای شما مفید بود است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای جاوا (Java)
- مجموعه آموزشهای برنامهنویسی
- گنجینه آموزشهای جاوا (Java)
- آشنایی با امکانات جدید جاوا ۱۴ — راهنمای کاربردی
- آموزش جامع برنامه نویسی جاوا به زبان ساده — بخش اول: مقدمه
==