ScopedValue جایگزینی برای ThreadLocal است و با VirtualThreads و StructuredTaskScope جدید کار می کند. دریابید که مقادیر محدوده می توانند برای برنامه های چند رشته ای شما در جاوا چه کاری انجام دهند.
- مقادیر همزمانی ساختاریافته و دامنه
- ScopedValue به عنوان جایگزینی برای ThreadLocal
- نحوه استفاده از نمونه ScopedValue
- استفاده از ScopedValue با StructuredTaskScope
- مقادیر محدوده در دنیای واقعی
- نتیجهگیری
همانطور که در InfoWorld گزارش شده است، جاوا ۲۲ چندین ویژگی جدید مرتبط با رشته را معرفی می کند. یکی از مهمترین آنها، نحو جدید ScopedValue
برای برخورد با مقادیر مشترک در زمینههای چند رشتهای است. بیایید نگاهی بیندازیم.
مقادیر همزمانی ساختاریافته و دامنه
ScopedValue
راه جدیدی برای دستیابی به رفتاری شبیه ThreadLocal
است. هر دو عنصر نیاز به ایجاد دادههایی را که به طور ایمن در یک رشته به اشتراک گذاشته میشوند را برطرف میکنند، اما هدف ScopedValue
سادگی بیشتر است. این طراحی شده است تا با VirtualThreads و StructuredTaskScope جدید کار کند، که در کنار هم threading را ساده کرده و آن را قدرتمندتر می کند. همانطور که این ویژگی های جدید به طور منظم استفاده می شوند، ویژگی مقادیر scoped برای رفع نیاز افزایش یافته به مدیریت اشتراک گذاری داده ها در رشته ها در نظر گرفته شده است.
ScopedValue به عنوان جایگزینی برای ThreadLocal
در یک برنامه چند رشته ای، اغلب باید متغیری را که به طور منحصر به فرد برای رشته فعلی وجود دارد، اعلام کنید. فکر کنید این شبیه به یک Singleton است – یک نمونه در هر برنامه – به جز اینکه یک نمونه در هر رشته باشد.
اگرچه ThreadLocal
در بسیاری از موارد به خوبی کار می کند، اما محدودیت هایی دارد. این مشکلات به عملکرد نخ و بار ذهنی برای توسعه دهنده خلاصه می شود. هر دو مشکل احتمالاً با استفاده توسعه دهندگان از ویژگی جدید VirtualThreads
و معرفی رشته های بیشتر به برنامه های خود افزایش خواهند یافت. JEP برای مقادیر scoped به خوبی محدودیتهای رشتههای مجازی را توصیف میکند.
یک نمونه ScopedValue
آنچه را که با استفاده از ThreadLocal
ممکن است به سه روش بهبود میبخشد:
- غیرقابل تغییر است.
- این توسط نخ های فرزند به ارث می رسد.
- هنگامی که روش حاوی کامل شد، به طور خودکار حذف می شود.
همانطور که مشخصات ScopedValue می گوید:
طول عمر این متغیرهای هر رشته باید محدود باشد: هر داده ای که از طریق متغیر هر رشته به اشتراک گذاشته می شود، پس از اتمام روشی که در ابتدا داده ها را به اشتراک گذاشته بود، غیرقابل استفاده می شود.
این با مراجع ThreadLocal
متفاوت است، که تا زمانی که خود رشته به پایان برسد یا متد ThreadLocal.remove()
فراخوانی شود، ادامه دارد.
تغییر ناپذیری هم پیروی از منطق یک نمونه ScopedValue
را آسانتر میکند و هم JVM را قادر میسازد تا آن را به شدت بهینه کند.
مقادیر Scoped از یک فراخوانی تابعی (lambda) برای تعریف عمر متغیر استفاده میکنند که یک رویکرد غیرمعمول در جاوا است. ممکن است در ابتدا عجیب به نظر برسد، اما در عمل، بسیار خوب کار می کند.
نحوه استفاده از یک نمونه ScopedValue
دو جنبه برای استفاده از یک نمونه ScopedValue
وجود دارد: ارائه و مصرف. ما می توانیم این را در سه قسمت در کد زیر مشاهده کنیم.
مرحله ۱: ScopedValue
را اعلام کنید:
final static ScopedValue<...> MY_SCOPED_VALUE = ScopedValue.newInstance();
مرحله ۲: نمونه ScopedValue
را پر کنید:
ScopedValue.where(MY_SCOPED_VALUE, ACTUAL_VALUE).run(() -> {
/* ...Code that accesses ACTUAL_VALUE... */
});
مرحله ۳: نمونه ScopedValue
را مصرف کنید (کدی که در مرحله ۲ در انتهای خط فراخوانی می شود):
var fooBar = DeclaringClass.MY_SCOPED_VALUE
جالب ترین بخش این فرآیند فراخوانی به ScopedValue.where()
است. این به شما امکان میدهد ScopedValue
اعلامشده را با یک مقدار واقعی مرتبط کنید و سپس متد .run()
را فراخوانی کنید و یک تابع callback ارائه دهید که با مقدار تعریف شده برای نمونه ScopedValue
.
به خاطر داشته باشید: مقدار واقعی مرتبط با نمونه ScopedValue
با توجه به رشته ای که در حال اجرا است تغییر می کند. به همین دلیل است که ما همه این کارها را انجام می دهیم! (مجموعه متغیر ویژه رشته در ScopedValue
گاهی اوقات تجسم آن نامیده می شود.)
یک مثال کد اغلب ارزش هزار کلمه را دارد، پس بیایید نگاهی بیندازیم. در کد زیر چندین رشته ایجاد می کنیم و برای هر کدام یک عدد تصادفی ایجاد می کنیم. سپس از یک نمونه ScopedValue
برای اعمال آن مقدار به یک متغیر مرتبط با رشته استفاده می کنیم:
import java.util.concurrent.ThreadLocalRandom;
public class Simple {
static final ScopedValue<Integer> RANDOM_NUMBER = ScopedValue.newInstance();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
int randomNumber = ThreadLocalRandom.current().nextInt(1, 101);
ScopedValue.where(RANDOM_NUMBER, randomNumber).run(() -> {
System.out.printf("Thread %s: Random number: %d\n", Thread.currentThread().getName(), RANDOM_NUMBER.get());
});
}).start();
}
}
}
تماس نهایی ثابت ScopedValue
RANDOM_NUMBER ScopedValue
را به ما می دهد تا در هر جایی از برنامه استفاده شود. در هر رشته، یک عدد تصادفی تولید میکنیم و آن را به RANDOM_NUMBER
مرتبط میکنیم.
سپس، داخل ScopedValue.where()
اجرا می کنیم. همه کدهای داخل کنترل کننده RANDOM_NUMBER
را به کد خاصی که در رشته فعلی تنظیم شده است، حل می کند. در مورد ما، ما فقط thread و شماره آن را به کنسول خروجی می دهیم.
یک اجرا به این صورت است:
$ javac --release 23 --enable-preview Simple.java
Note: Simple.java uses preview features of Java SE 23.
Note: Recompile with -Xlint:preview for details.
$ java --enable-preview Simple
Thread Thread-1: Random number: 45
Thread Thread-2: Random number: 100
Thread Thread-3: Random number: 51
Thread Thread-4: Random number: 74
Thread Thread-5: Random number: 37
Thread Thread-0: Random number: 32
Thread Thread-6: Random number: 28
Thread Thread-7: Random number: 43
Thread Thread-8: Random number: 95
Thread Thread-9: Random number: 21
توجه داشته باشید که برای اجرای این کد در حال حاضر باید سوئیچ enable-preview
را روشن کنیم. پس از ارتقاء ویژگی مقادیر دامنه، این کار ضروری نخواهد بود.
هر رشته یک نسخه مجزا از RANDOM_NUMBER
دریافت می کند. هر جا که به آن مقدار دسترسی داشته باشید، مهم نیست که چقدر عمیق تو در تو باشد، همان نسخه را دریافت می کند — تا زمانی که از داخل آن run()
callback نشات می گیرد.
در یک مثال ساده، می توانید تصور کنید که مقدار تصادفی را به عنوان پارامتر متد ارسال کنید. با این حال، همانطور که کد برنامه رشد می کند، به سرعت غیرقابل مدیریت می شود و منجر به اجزای محکم می شود. استفاده از یک نمونه ScopedValue
یک راه آسان برای دسترسی جهانی به متغیر است در حالی که آن را محدود به مقدار معینی برای رشته فعلی نگه میدارید.
استفاده از ScopedValue با StructuredTaskScope
از آنجایی که ScopedValue
برای آسانتر کردن کار با تعداد بالای رشتههای مجازی طراحی شده است، و StructuredTaskScope
یک روش توصیهشده برای استفاده از رشتههای مجازی است، باید بدانید که چگونه این دو ویژگی را با هم ترکیب کنید.
فرآیند کلی ترکیب ScopedValue
با StructuredTaskScope
شبیه به مثال Thread
قبلی ما است. فقط نحو متفاوت است:
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.StructuredTaskScope;
public class ThreadScoped {
static final ScopedValue<Integer> RANDOM_NUMBER = ScopedValue.newInstance();
public static void main(String[] args) throws Exception {
try (StructuredTaskScope scope = new StructuredTaskScope()) {
for (int i = 0; i < 10; i++) {
scope.fork(() -> {
int randomNumber = ThreadLocalRandom.current().nextInt(1, 101);
ScopedValue.where(RANDOM_NUMBER, randomNumber).run(() -> {
System.out.printf("Thread %s: Random number: %d\n", Thread.currentThread().threadId(), RANDOM_NUMBER.get());
});
return null;
});
}
scope.join();
}
}
}
ساختار کلی یکسان است: ScopedValue
را تعریف می کنیم و سپس رشته هایی ایجاد می کنیم و از ScopedValue (RANDOM_NUMBER)
در آنها استفاده می کنیم. به جای ایجاد اشیاء Thread
، از scope.fork()
استفاده می کنیم.
توجه کنید که ما null را از لامبدا که به scope.fork()
ارسال میکنیم، برمیگردانیم، زیرا در مورد ما از مقدار بازگشتی استفاده نمیکنیم – فقط یک رشته را به کنسول خروجی میدهیم. ممکن است مقداری را از scope.fork()
برگردانید و از آن استفاده کنید.
همچنین، توجه داشته باشید که من به تازگی Exception
را از main()
پرتاب کردم. روش scope.fork()
یک InterruptedException
را پرتاب می کند که باید در کد تولید به درستی مدیریت شود.
نمونه بالا برای StructuredTaskScope
و ScopedValue
معمولی است. همانطور که می بینید، آنها به خوبی با هم کار می کنند – در واقع، آنها برای آن طراحی شده اند.
مقادیر محدوده در دنیای واقعی
ما نمونههای ساده را بررسی کردهایم تا ببینیم ویژگی مقادیر دامنهدار چگونه کار میکند. اکنون بیایید به این فکر کنیم که چگونه در سناریوهای پیچیده تر کار می کند. به ویژه، مقدارهای محدوده JEP استفاده را در یک برنامه وب بزرگ که در آن بسیاری از مؤلفهها در حال تعامل هستند، برجسته میکند. مؤلفه رسیدگی به درخواست می تواند مسئول به دست آوردن یک شی کاربر (یک «اصل») باشد که نشان دهنده مجوز برای درخواست حاضر است. این یک مدل رشته به ازای درخواست است که توسط بسیاری از فریم ورک ها استفاده می شود. رشتههای مجازی با جدا کردن رشتههای JVM از رشتههای سیستم عامل، این را بسیار مقیاسپذیرتر میکنند.
هنگامی که کنترل کننده درخواست شی کاربر را به دست آورد، می تواند آن را با حاشیه نویسی ScopedValue
در معرض بقیه برنامه قرار دهد. سپس، هر مؤلفه دیگری که از داخل callback where()
فراخوانی میشود، میتواند به شی کاربر thread خاص دسترسی داشته باشد. به عنوان مثال، در JEP، نمونه کد یک مؤلفه DBAccess
را نشان می دهد که برای تأیید مجوز به PRINCIPAL
ScopedValue
متکی است:
/** https://openjdk.org/jeps/429 */
class Server {
final static ScopedValue<Principal> PRINCIPAL = ScopedValue.newInstance();
void serve(Request request, Response response) {
var level = (request.isAdmin() ? ADMIN : GUEST);
var principal = new Principal(level);
ScopedValue.where(PRINCIPAL, principal)
.run(() -> Application.handle(request, response));
}
}
class DBAccess {
DBConnection open() {
var principal = Server.PRINCIPAL.get();
if (!principal.canOpen()) throw new InvalidPrincipalException();
return newConnection(...);
}
}
در اینجا می توانید همان خطوط کلی نمونه اول ما را ببینید. تنها تفاوت این است که اجزای مختلف وجود دارد. متد serve()
تصور میشود که در هر درخواست رشتههایی ایجاد میکند، و در جایی پایین تر، فراخوانی Application.handle()
با DBAccess.open تعامل خواهد کرد. ()
. از آنجا که تماس از داخل ScopedValue.where()
منشا می گیرد، می دانیم که PRINCIPAL
به مقدار تنظیم شده برای این رشته توسط Principal(level) جدید حل می شود.
تماس بگیرید.
نکته مهم دیگر این است که تماسهای بعدی به scope.fork()
نمونههای ScopedValue
تعریفشده توسط والدین را به ارث خواهند برد. بنابراین، برای مثال، حتی اگر روش serve()
بالا scope.fork()
را فراخوانی کند و به وظیفه فرزند در PRINCIPAL.get()< دسترسی داشته باشد. /code>، همان مقدار thread-bound را به عنوان والد دریافت می کند. (شما می توانید شبه کد این مثال را در بخش "ارث بری مقادیر محدوده" JEP ببینید.)
تغییرناپذیری مقادیر دامنه به این معنی است که JVM میتواند این اشتراکگذاری نخهای فرزند را بهینه کند، بنابراین میتوانیم در این موارد انتظار سربار کارایی پایینی داشته باشیم.
نتیجه گیری
اگرچه همزمانی چند رشته ای ذاتاً پیچیده است، ویژگی های جدیدتر جاوا کمک زیادی به ساده تر و قدرتمندتر کردن آن می کند. ویژگی جدید scoped values یکی دیگر از ابزارهای موثر برای جعبه ابزار توسعه دهنده جاوا است. در مجموع، جاوا ۲۲ یک رویکرد هیجانانگیز و بهطور اساسی بهبود یافته برای threading در جاوا ارائه میدهد.
پست های مرتبط
درک ScopedValue جدید جاوا
درک ScopedValue جدید جاوا
درک ScopedValue جدید جاوا