مقدمة
لقد نظرنا بالفعل في كيفية إنشاء سلاسل الرسائل في
الجزء الأول . دعونا نتذكر مرة أخرى.
مؤشر الترابط هو
Thread
شيء يتم تشغيله فيه
run
، لذلك دعونا نستخدم
برنامج التحويل البرمجي الخاص بـ Tutorialspoint Java ونقوم بتنفيذ التعليمات البرمجية التالية:
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 . أي أن
Task
هذا هو ما سيتم تنفيذه فيه
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
النتيجة من المشكلة
task
.
(!)مهم، أنه في لحظة الحصول على النتيجة باستخدام الطريقة،
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);
}
};
هناك الكثير من التعليمات البرمجية غير الضرورية، أليس كذلك؟ تؤدي كل فئة من الفئات المعلنة وظيفة واحدة، ولكن لوصفها نستخدم مجموعة من التعليمات البرمجية المساعدة غير الضرورية. وقد اعتقد مطورو Java ذلك أيضًا. لذلك، قدموا مجموعة من "الواجهات الوظيفية" (
@FunctionalInterface
) وقرروا أن Java نفسها سوف "تفكر" في كل شيء لنا، باستثناء المهم:
Supplier<String> supplier = () -> "String";
Consumer<String> consumer = s -> System.out.println(s);
Function<String, Integer> converter = s -> Integer.valueOf(s);
Supplier
- مزود. ليس لها أي معلمات، ولكنها تُرجع شيئًا ما، أي أنها توفره.
Consumer
- مستهلك. يأخذ شيئًا ما كمدخل (معلمات) ويفعل شيئًا به، أي أنه يستهلك شيئًا ما. هناك وظيفة أخرى. يأخذ شيئًا ما كمدخل (معلمة
s
)، ويفعل شيئًا ويعيد شيئًا ما. كما نرى، يتم استخدام الأدوية الجنيسة بنشاط. إذا لم تكن متأكدًا، يمكنك تذكرها وقراءة "
نظرية الأدوية الجنيسة في Java أو كيفية وضع الأقواس موضع التنفيذ ."
المستقبل الكامل
مع مرور الوقت، قدمت Java 1.8 فئة جديدة تسمى
CompletableFuture
. إنه ينفذ الواجهة
Future
، مما يعني أنه
task
سيتم تنفيذ الواجهة الخاصة بنا في المستقبل ويمكننا التنفيذ
get
والحصول على النتيجة. لكنه ينفذ أيضًا بعض ملفات
CompletionStage
. من الترجمة، غرضها واضح بالفعل: إنها مرحلة معينة من نوع ما من الحساب. يمكن العثور على مقدمة موجزة للموضوع في النظرة العامة "
مقدمة إلى مرحلة الإكمال والمستقبل الكامل ". دعنا نصل مباشرة إلى هذه النقطة. دعونا نلقي نظرة على قائمة الطرق الثابتة المتاحة لمساعدتنا على البدء:
فيما يلي خيارات استخدامها:
import java.util.concurrent.CompletableFuture;
public class App {
public static void main(String []args) throws Exception {
CompletableFuture<String> completed;
completed = CompletableFuture.completedFuture("Просто meaning");
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 "Значение";
});
}
}
إذا قمنا بتشغيل هذا الكود، فسنرى أن الإنشاء
CompletableFuture
يتضمن بدء السلسلة بأكملها. لذلك، على الرغم من وجود بعض التشابه مع SteamAPI من Java8، فإن هذا هو الفرق بين هذه الأساليب. على سبيل المثال:
List<String> array = Arrays.asList("one", "two");
Stream<String> stringStream = array.stream().map(value -> {
System.out.println("Executed");
return value.toUpperCase();
});
هذا مثال على Java 8 Stream Api (يمكنك قراءة المزيد عنها هنا "
دليل Java 8 Stream API بالصور والأمثلة "). إذا قمت بتشغيل هذا الرمز، فلن
Executed
يتم عرضه. أي أنه عند إنشاء دفق في Java، لا يبدأ الدفق على الفور، ولكنه ينتظر حتى تكون القيمة مطلوبة منه. ولكنها
CompletableFuture
تبدأ السلسلة للتنفيذ فورًا، دون انتظار أن يُطلب منها القيمة المحسوبة. أعتقد أنه من المهم أن نفهم هذا. لذلك لدينا مستقبل كامل. كيف يمكننا إنشاء سلسلة وما هي الوسائل المتوفرة لدينا؟ دعونا نتذكر الواجهات الوظيفية التي كتبنا عنها سابقًا.
- لدينا دالة (
Function
) تأخذ A وترجع B. ولها طريقة واحدة - apply
(apply).
- لدينا مستهلك (
Consumer
) يقبل A ولا يُرجع شيئًا (Void). لها طريقة واحدة فقط - accept
(القبول).
- لدينا كود يعمل على موضوع
Runnable
لا يقبل أو يعود. لها طريقة واحدة - run
(تشغيل).
الشيء الثاني الذي يجب تذكره هو أنه يستخدم المستهلكين والوظائف
CompletalbeFuture
في عمله .
Runnable
بالنظر إلى هذا، يمكنك دائمًا أن تتذكر أنه يمكنك
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
_
Async
وهذا يعني أنه سيتم تنفيذ هذه المراحل في موضوع جديد. سيتم أخذه من تجمع خاص، لذلك لا يُعرف مسبقًا نوع التدفق الذي سيكون عليه، جديدًا أم قديمًا. كل هذا يتوقف على مدى صعوبة المهام. بالإضافة إلى هذه الأساليب، هناك ثلاثة احتمالات أكثر إثارة للاهتمام. وللتوضيح، لنتخيل أن لدينا خدمة معينة تتلقى رسالة من مكان ما ويستغرق ذلك وقتًا:
public static class NewsService {
public static String getMessage() {
try {
Thread.currentThread().sleep(3000);
return "Message";
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
الآن دعونا نلقي نظرة على الميزات الأخرى التي
CompletableFuture
. يمكننا دمج النتيجة
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
، نستخدم انتظار النتيجة. ولا يمكننا فقط الجمع بين (combine) ولكن أيضًا إرجاع
CompletableFuture
:
CompletableFuture.completedFuture(2L)
.thenCompose((val) -> CompletableFuture.completedFuture(val + 2))
.thenAccept(result -> System.out.println(result));
وهنا أود أن أشير إلى أنه تم استخدام الطريقة للإيجاز
CompletableFuture.completedFuture
. لا تنشئ هذه الطريقة سلسلة رسائل جديدة، لذا سيتم تنفيذ بقية السلسلة في نفس السلسلة التي تم استدعاؤها فيها
completedFuture
. هناك أيضا طريقة
thenAcceptBoth
. إنه مشابه جدًا لـ
accept
، ولكن إذا
thenAccept
قبل
consumer
، فإنه
thenAcceptBoth
يقبل
CompletableStage
+ آخر كمدخل
BiConsumer
، أي
consumer
الذي يقبل مصدرين كمدخل، وليس مصدرًا واحدًا. هناك احتمال آخر مثير للاهتمام فيما يتعلق بالكلمة
Either
:
تقبل هذه الأساليب بديلاً
CompletableStage
وسيتم تنفيذها على الطريقة
CompletableStage
التي يتم تنفيذها أولاً. وأود أن أنهي هذه المراجعة بميزة أخرى مثيرة للاهتمام
CompletableFuture
- وهي معالجة الأخطاء.
CompletableFuture.completedFuture(2L)
.thenApply((a) -> {
throw new IllegalStateException("error");
}).thenApply((a) -> 3L)
.thenAccept(val -> System.out.println(val));
هذا الكود لن يفعل أي شيء، لأن... سيتم طرح استثناء ولن يحدث شيء. ولكن إذا قمنا بإلغاء التعليق
exceptionally
، فإننا نحدد السلوك.
CompletableFuture
وأوصي أيضًا بمشاهدة الفيديو التالي حول هذا الموضوع :
في رأيي المتواضع، تعد مقاطع الفيديو هذه من أكثر مقاطع الفيديو المرئية على الإنترنت. يجب أن يكون واضحا منهم كيف يعمل كل شيء، وما هي الترسانة التي لدينا ولماذا نحن بحاجة إليها.
خاتمة
نأمل أن يكون من الواضح الآن كيف يمكن استخدام سلاسل الرسائل لاسترداد الحسابات بعد حسابها. مواد اضافية:
# فياتشيسلاف
GO TO FULL VERSION