مقدمة
في
الجزء الأول
، استعرضنا كيفية إنشاء المواضيع. دعونا نتذكر مرة أخرى.
يتم تمثيل الخيط بواسطة فئة الخيط، التي
run()
يتم استدعاء أسلوبها. لذلك دعونا نستخدم
مترجم Java عبر الإنترنت Tutorialspoint
وننفذ التعليمات البرمجية التالية:
public class HelloWorld {
public static void main(String[] args) {
Runnable task = () -> {
System.out.println("Hello World");
};
new Thread(task).start();
}
}
هل هذا هو الخيار الوحيد لبدء مهمة في سلسلة رسائل؟
java.util.concurrent.Callable
اتضح أن
java.lang.Runnable
لديه أخ يدعى
java.util.concurrent.Callable
الذي ولد في Java 1.5. ما هي الاختلافات؟ إذا ألقيت نظرة فاحصة على Javadoc لهذه الواجهة، فسنرى أنه على عكس
Runnable
الواجهة الجديدة، تعلن الواجهة الجديدة عن
call()
طريقة تُرجع نتيجة. كما أنه يطرح استثناءً بشكل افتراضي. وهذا يعني أنه يوفر علينا الاضطرار إلى
try-catch
حظر الاستثناءات المحددة. ليس سيئا، أليس كذلك؟ الآن لدينا مهمة جديدة بدلاً من
Runnable
:
Callable task = () -> {
return "Hello, World!";
};
ولكن ماذا نفعل بها؟ لماذا نحتاج إلى مهمة تعمل على سلسلة رسائل تُرجع نتيجة؟ ومن الواضح أنه بالنسبة لأي إجراءات يتم تنفيذها في المستقبل، فإننا نتوقع الحصول على نتيجة تلك الإجراءات في المستقبل. ولدينا واجهة بالاسم المقابل:
java.util.concurrent.Future
java.util.concurrent.Future
تحدد الواجهة
java.util.concurrent.Future
واجهة برمجة التطبيقات (API) للعمل مع المهام التي نخطط لتلقي نتائجها في المستقبل: طرق الحصول على النتيجة وطرق التحقق من الحالة. فيما يتعلق بـ
Future
، نحن مهتمون بتنفيذه في فئة
java.util.concurrent.FutureTask
. هذه هي "المهمة" التي سيتم تنفيذها في
Future
. ما يجعل هذا التنفيذ أكثر إثارة للاهتمام هو أنه ينفذ أيضًا Runnable. يمكنك اعتبار هذا نوعًا من المحول بين النموذج القديم للعمل مع المهام على سلاسل الرسائل والنموذج الجديد (الجديد بمعنى أنه ظهر في Java 1.5). هنا مثال:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class HelloWorld {
public static void main(String[] args) throws Exception {
Callable task = () -> {
return "Hello, World!";
};
FutureTask<String> future = new FutureTask<>(task);
new Thread(future).start();
System.out.println(future.get());
}
}
كما ترون من المثال، نستخدم الطريقة
get
للحصول على النتيجة من المهمة.
ملحوظة:عندما تحصل على النتيجة باستخدام
get()
الطريقة، يصبح التنفيذ متزامنًا! ما هي الآلية التي تعتقد أنه سيتم استخدامها هنا؟ صحيح، لا يوجد كتلة المزامنة. لهذا السبب لن نرى
الانتظار
في JVisualVM كطريقة مألوفة
monitor
( لأن الآلية قيد الاستخدام).
wait
park()
LockSupport
واجهات وظيفية
بعد ذلك، سنتحدث عن فئات من Java 1.8، لذا من الأفضل أن نقدم مقدمة مختصرة. انظر إلى الكود التالي:
Supplier<String> supplier = new Supplier<String>() {
@Override
public String get() {
return "String";
}
};
Consumer<String> consumer = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
Function<String, Integer> converter = new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return Integer.valueOf(s);
}
};
الكثير والكثير من التعليمات البرمجية الإضافية، ألا تقول ذلك؟ تؤدي كل فئة من الفئات المعلنة وظيفة واحدة، لكننا نستخدم مجموعة من التعليمات البرمجية الداعمة الإضافية لتعريفها. وهذه هي الطريقة التي فكر بها مطورو جافا. وبناءً على ذلك، قدموا مجموعة من "الواجهات الوظيفية" (
@FunctionalInterface
) وقرروا أن جافا نفسها ستتولى "التفكير"، تاركة فقط الأشياء المهمة لنا لنقلق بشأنها:
Supplier<String> supplier = () -> "String";
Consumer<String> consumer = s -> System.out.println(s);
Function<String, Integer> converter = s -> Integer.valueOf(s);
اللوازم
Supplier
. لا تحتوي على معلمات، ولكنها تُرجع شيئًا ما. هذه هي الطريقة التي تزود بها الأشياء. يستهلك
Consumer
. فهو يأخذ شيئًا ما كمدخل (حجة) ويفعل شيئًا به. الحجة هي ما تستهلكه. ثم لدينا أيضا
Function
. فهو يأخذ مدخلات (وسائط)، ويفعل شيئًا ما، ويعيد شيئًا ما. يمكنك أن ترى أننا نستخدم الأدوية الجنيسة بنشاط. إذا لم تكن متأكدًا، يمكنك الحصول على معلومات تجديدية من خلال قراءة "
الأدوية العامة في Java: كيفية استخدام الأقواس الزاوية عمليًا
".
المستقبل الكامل
مر الوقت وظهرت فئة جديدة تسمى
CompletableFuture
في Java 1.8. يقوم بتنفيذ
Future
الواجهة، أي أن مهامنا ستكتمل في المستقبل، ويمكننا الاتصال
get()
للحصول على النتيجة. ولكنه ينفذ أيضًا
CompletionStage
الواجهة. الاسم يقول كل شيء: هذه مرحلة معينة من مجموعة من الحسابات. يمكن العثور على مقدمة موجزة للموضوع في المراجعة هنا: مقدمة إلى CompletionStage وCompletableFuture. دعونا نصل إلى هذه النقطة. دعونا نلقي نظرة على قائمة الأساليب الثابتة المتاحة التي ستساعدنا على البدء:
فيما يلي خيارات لاستخدامها:
import java.util.concurrent.CompletableFuture;
public class App {
public static void main(String[] args) throws Exception {
CompletableFuture<String> completed;
completed = CompletableFuture.completedFuture("Just a value");
CompletableFuture<Void> voidCompletableFuture;
voidCompletableFuture = CompletableFuture.runAsync(() -> {
System.out.println("run " + Thread.currentThread().getName());
});
CompletableFuture<String> supplier;
supplier = CompletableFuture.supplyAsync(() -> {
System.out.println("supply " + Thread.currentThread().getName());
return "Value";
});
}
}
إذا قمنا بتنفيذ هذا الكود، فسنرى أن إنشاء ملف
CompletableFuture
يتضمن أيضًا إطلاق مسار كامل. لذلك، مع بعض التشابه مع SteamAPI من Java8، هذا هو المكان الذي نجد فيه الفرق بين هذه الأساليب. على سبيل المثال:
List<String> array = Arrays.asList("one", "two");
Stream<String> stringStream = array.stream().map(value -> {
System.out.println("Executed");
return value.toUpperCase();
});
هذا مثال على Stream API الخاص بـ Java 8. إذا قمت بتشغيل هذا الرمز، سترى أنه لن يتم عرض "تم التنفيذ". بمعنى آخر، عند إنشاء دفق في Java، لا يبدأ الدفق على الفور. وبدلا من ذلك، فإنه ينتظر أن يريد شخص ما قيمة منه. ولكن
CompletableFuture
يبدأ في تنفيذ خط الأنابيب على الفور، دون انتظار أن يطلب منه أحد القيمة. أعتقد أن هذا من المهم أن نفهم. لذا، لدينا
CompletableFuture
. كيف يمكننا عمل خط أنابيب (أو سلسلة) وما هي الآليات المتوفرة لدينا؟ تذكر تلك الواجهات الوظيفية التي كتبنا عنها سابقًا.
- لدينا
Function
الذي يأخذ A ويعيد B. وله طريقة واحدة: apply()
.
- لدينا
Consumer
يأخذ A ولا يُرجع شيئًا (Void). وله طريقة واحدة: accept()
.
- لدينا
Runnable
، الذي يعمل على الخيط، ولا يأخذ شيئًا ولا يُرجع شيئًا. وله طريقة واحدة: run()
.
والشيء التالي الذي يجب تذكره هو أنه
CompletableFuture
يستخدم
Runnable
و
Consumers
و و
Functions
في عمله. وبناء على ذلك، يمكنك دائمًا معرفة أنه يمكنك القيام بما يلي من خلال
CompletableFuture
:
public static void main(String[] args) throws Exception {
AtomicLong longValue = new AtomicLong(0);
Runnable task = () -> longValue.set(new Date().getTime());
Function<Long, Date> dateConverter = (longvalue) -> new Date(longvalue);
Consumer<Date> printer = date -> {
System.out.println(date);
System.out.flush();
};
CompletableFuture.runAsync(task)
.thenApply((v) -> longValue.get())
.thenApply(dateConverter)
.thenAccept(printer);
}
الأساليب
thenRun()
و
thenApply()
و و
thenAccept()
لها إصدارات "غير متزامنة". وهذا يعني أنه سيتم الانتهاء من هذه المراحل على موضوع مختلف. سيتم أخذ هذا الموضوع من مجموعة خاصة — لذلك لن نعرف مقدمًا ما إذا كان سيكون موضوعًا جديدًا أم قديمًا. كل هذا يتوقف على مدى كثافة المهام الحسابية. بالإضافة إلى هذه الأساليب، هناك ثلاثة احتمالات أكثر إثارة للاهتمام. من أجل الوضوح، دعونا نتخيل أن لدينا خدمة معينة تتلقى نوعا من الرسائل من مكان ما - وهذا يستغرق وقتا:
public static class NewsService {
public static String getMessage() {
try {
Thread.currentThread().sleep(3000);
return "Message";
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
الآن، دعونا نلقي نظرة على القدرات الأخرى التي
CompletableFuture
توفرها. يمكننا دمج نتيجة a
CompletableFuture
مع نتيجة أخرى
CompletableFuture
:
Supplier newsSupplier = () -> NewsService.getMessage();
CompletableFuture<String> reader = CompletableFuture.supplyAsync(newsSupplier);
CompletableFuture.completedFuture("!!")
.thenCombine(reader, (a, b) -> b + a)
.thenAccept(result -> System.out.println(result))
.get();
لاحظ أن الخيوط هي خيوط خفية بشكل افتراضي، لذا من أجل الوضوح، نستخدم
get()
انتظار النتيجة. لا يمكننا الجمع فحسب
CompletableFutures
، بل يمكننا أيضًا إرجاع
CompletableFuture
:
CompletableFuture.completedFuture(2L)
.thenCompose((val) -> CompletableFuture.completedFuture(val + 2))
.thenAccept(result -> System.out.println(result));
هنا أريد أن أشير إلى أنه
CompletableFuture.completedFuture()
تم استخدام الطريقة للإيجاز. لا تنشئ هذه الطريقة سلسلة رسائل جديدة، لذا سيتم تنفيذ بقية المسار على نفس سلسلة المحادثات التي
completedFuture
تم استدعاؤها. هناك أيضا
thenAcceptBoth()
طريقة. إنه مشابه جدًا لـ
accept()
، ولكن إذا
thenAccept()
قبل a
Consumer
، فإنه يقبل +
thenAcceptBoth()
آخر كمدخل، أي أن a يأخذ مصدرين بدلاً من مصدر واحد. هناك قدرة أخرى مثيرة للاهتمام توفرها الطرق التي يتضمن اسمها كلمة "إما": تقبل هذه الطرق بديلاً ويتم تنفيذها على ما يتم تنفيذه أولاً. أخيرًا، أريد إنهاء هذه المراجعة بميزة أخرى مثيرة للاهتمام وهي : معالجة الأخطاء.
CompletableStage
BiConsumer
consumer
CompletableStage
CompletableStage
CompletableFuture
CompletableFuture.completedFuture(2L)
.thenApply((a) -> {
throw new IllegalStateException("error");
}).thenApply((a) -> 3L)
.thenAccept(val -> System.out.println(val));
لن يفعل هذا الرمز شيئًا، لأنه سيكون هناك استثناء ولن يحدث أي شيء آخر. ولكن من خلال إلغاء التعليق على العبارة "استثنائيًا"، فإننا نحدد السلوك المتوقع. وبالحديث عن ذلك
CompletableFuture
، أنصحك أيضًا بمشاهدة الفيديو التالي:
في رأيي المتواضع، هذه من بين أكثر مقاطع الفيديو التوضيحية على الإنترنت. وينبغي لهم أن يوضحوا كيف يعمل كل هذا، وما هي مجموعة الأدوات المتوفرة لدينا، ولماذا نحتاج إلى كل هذا.
خاتمة
نأمل أن يكون من الواضح الآن كيف يمكنك استخدام سلاسل الرسائل للحصول على العمليات الحسابية بعد اكتمالها. مواد اضافية:
أفضل معًا: Java وفئة Thread. الجزء الأول - خيوط التنفيذ
أفضل معًا: Java وفئة Thread. الجزء الثاني – التزامن
بشكل أفضل معًا: Java وفئة Thread. الجزء الثالث - التفاعل
بشكل أفضل معًا: Java وفئة Thread. الجزء الخامس — Executor، ThreadPool، Fork/Join
Better Together: Java وفئة Thread. الجزء السادس – أطلق النار بعيدًا!
GO TO FULL VERSION