CodeGym /مدونة جافا /Random-AR /أفضل معًا: Java وفئة Thread. الجزء الرابع – القابل للاستد...
John Squirrels
مستوى
San Francisco

أفضل معًا: Java وفئة Thread. الجزء الرابع – القابل للاستدعاء والمستقبل والأصدقاء

نشرت في المجموعة

مقدمة

في الجزء الأول ، استعرضنا كيفية إنشاء المواضيع. دعونا نتذكر مرة أخرى. أفضل معًا: Java وفئة Thread.  الجزء الرابع - القابلون للاستدعاء والمستقبل والأصدقاء - 1يتم تمثيل الخيط بواسطة فئة الخيط، التي 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( لأن الآلية قيد الاستخدام). waitpark()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. دعونا نصل إلى هذه النقطة. دعونا نلقي نظرة على قائمة الأساليب الثابتة المتاحة التي ستساعدنا على البدء: أفضل معًا: Java وفئة Thread.  الجزء الرابع - القابلون للاستدعاء والمستقبل والأصدقاء - 2فيما يلي خيارات لاستخدامها:

import java.util.concurrent.CompletableFuture;
public class App {
    public static void main(String[] args) throws Exception {
        // A CompletableFuture that already contains a Result
        CompletableFuture<String> completed;
        completed = CompletableFuture.completedFuture("Just a value");
        // A CompletableFuture that runs a new thread from Runnable. That's why it's Void
        CompletableFuture<Void> voidCompletableFuture;
        voidCompletableFuture = CompletableFuture.runAsync(() -> {
            System.out.println("run " + Thread.currentThread().getName());
        });
        // A CompletableFuture that starts a new thread whose result we'll get from a Supplier 
        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 computation
        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 يأخذ مصدرين بدلاً من مصدر واحد. هناك قدرة أخرى مثيرة للاهتمام توفرها الطرق التي يتضمن اسمها كلمة "إما": تقبل هذه الطرق بديلاً ويتم تنفيذها على ما يتم تنفيذه أولاً. أخيرًا، أريد إنهاء هذه المراجعة بميزة أخرى مثيرة للاهتمام وهي : معالجة الأخطاء. CompletableStageBiConsumerconsumerأفضل معًا: Java وفئة Thread.  الجزء الرابع - القابلون للاستدعاء والمستقبل والأصدقاء - 3CompletableStageCompletableStageCompletableFuture

CompletableFuture.completedFuture(2L)
				 .thenApply((a) -> {
					throw new IllegalStateException("error");
				 }).thenApply((a) -> 3L)
				 //.exceptionally(ex -> 0L)
				 .thenAccept(val -> System.out.println(val));
لن يفعل هذا الرمز شيئًا، لأنه سيكون هناك استثناء ولن يحدث أي شيء آخر. ولكن من خلال إلغاء التعليق على العبارة "استثنائيًا"، فإننا نحدد السلوك المتوقع. وبالحديث عن ذلك CompletableFuture، أنصحك أيضًا بمشاهدة الفيديو التالي: في رأيي المتواضع، هذه من بين أكثر مقاطع الفيديو التوضيحية على الإنترنت. وينبغي لهم أن يوضحوا كيف يعمل كل هذا، وما هي مجموعة الأدوات المتوفرة لدينا، ولماذا نحتاج إلى كل هذا.

خاتمة

نأمل أن يكون من الواضح الآن كيف يمكنك استخدام سلاسل الرسائل للحصول على العمليات الحسابية بعد اكتمالها. مواد اضافية: أفضل معًا: Java وفئة Thread. الجزء الأول - خيوط التنفيذ أفضل معًا: Java وفئة Thread. الجزء الثاني – التزامن بشكل أفضل معًا: Java وفئة Thread. الجزء الثالث - التفاعل بشكل أفضل معًا: Java وفئة Thread. الجزء الخامس — Executor، ThreadPool، Fork/Join Better Together: Java وفئة Thread. الجزء السادس – أطلق النار بعيدًا!
تعليقات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION