JavaRush /وبلاگ جاوا /Random-FA /شما نمی توانید جاوا را با یک موضوع خراب کنید: قسمت چهارم ...
Viacheslav
مرحله

شما نمی توانید جاوا را با یک موضوع خراب کنید: قسمت چهارم - Callable، Future و دوستان

در گروه منتشر شد

معرفی

قبلاً در قسمت اول نحوه ایجاد موضوعات را بررسی کرده ایم . دوباره به یاد بیاوریم. شما نمی توانید جاوا را با یک موضوع خراب کنید: قسمت چهارم - قابل فراخوانی، آینده و دوستان - 1یک رشته 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 است و در جاوا 1.5 به دنیا آمده است. چه تفاوت هایی دارند؟ اگر نگاه دقیق‌تری به JavaDoc این رابط بیندازیم، می‌بینیم که برخلاف Runnable, رابط جدید متدی را اعلام می‌کند callکه نتیجه را برمی‌گرداند. همچنین به طور پیش فرض Exception را می اندازد. یعنی ما را از نیاز به نوشتن try-catchبلوک برای استثناهای بررسی شده نجات می دهد. در حال حاضر بد نیست، درست است؟ Runnableاکنون به جای آن یک کار جدید داریم :
Callable task = () -> {
	return "Hello, World!";
};
اما با آن چه باید کرد؟ چرا ما حتی به وظیفه ای نیاز داریم که روی رشته ای اجرا شود که نتیجه را برمی گرداند؟ بدیهی است که در آینده انتظار دریافت نتیجه اقداماتی را داریم که در آینده انجام خواهد شد. آینده در انگلیسی - Future. و یک رابط دقیقاً با همین نام وجود دارد:java.util.concurrent.Future

java.util.concurrent.Future

رابط java.util.concurrent.Future یک API را برای کار با وظایفی که ما قصد داریم نتایج آنها را در آینده بدست آوریم توصیف می کند: روش هایی برای به دست آوردن نتایج، روش هایی برای بررسی وضعیت. ما Futureعلاقه مند به اجرای آن java.util.concurrent.FutureTask هستیم . یعنی Taskاین همان چیزی است که در اجرا خواهد شد Future. نکته جالب در مورد این پیاده سازی این است که پیاده سازی و Runnable. می توانید این را نوعی آداپتور مدل قدیمی کار با وظایف در رشته ها و مدل جدید (به معنای جدید در جاوا 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اجرا به صورت همزمان می شود. فکر می کنید در اینجا از چه مکانیزمی استفاده می شود؟ درست است، هیچ بلوک همگام‌سازی وجود ندارد - بنابراین ما WAITING را در JVisualVM نه به‌عنوان monitorیا wait، بلکه به‌عنوان همان بلوک park(از آنجایی که مکانیسم استفاده می‌شود LockSupport) خواهیم دید.

رابط های کاربردی

در ادامه در مورد کلاس های جاوا 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- مصرف كننده. چیزی را به عنوان ورودی (پارامتر s) می گیرد و با آن کاری انجام می دهد، یعنی چیزی را مصرف می کند. عملکرد دیگری نیز وجود دارد. چیزی را به عنوان ورودی (پارامتر) می گیرد s، کاری انجام می دهد و چیزی را برمی گرداند. همانطور که می بینیم، ژنریک ها به طور فعال مورد استفاده قرار می گیرند. اگر مطمئن نیستید، می توانید آنها را به خاطر بسپارید و " نظریه ژنریک در جاوا یا نحوه قرار دادن پرانتز در عمل " را بخوانید.

CompletableFuture

با گذشت زمان، جاوا 1.8 کلاس جدیدی به نام CompletableFuture. این رابط را پیاده سازی می کند Future، به این معنی که مال ما taskدر آینده اجرا می شود و ما می توانیم اجرا کنیم getو نتیجه را بگیریم. اما برخی را نیز اجرا می کند CompletionStage. از ترجمه، هدف آن از قبل مشخص است: این مرحله خاصی از نوعی محاسبه است. مقدمه کوتاهی برای موضوع را می توان در نمای کلی " مقدمه ای بر CompletionStage و CompletableFuture " یافت. یک راست بریم سر اصل مطلب. بیایید به لیستی از روش‌های استاتیک موجود برای کمک به شروع کار نگاه کنیم: شما نمی توانید جاوا را با یک موضوع خراب کنید: قسمت چهارم - قابل فراخوانی، آینده و دوستان - 2در اینجا گزینه‌هایی برای استفاده از آنها وجود دارد:
import java.util.concurrent.CompletableFuture;
public class App {
    public static void main(String []args) throws Exception {
        // CompletableFuture уже содержащий результат
        CompletableFuture<String> completed;
        completed = CompletableFuture.completedFuture("Просто meaning");
        // CompletableFuture, запускающий (run) новый поток с Runnable, поэтому он Void
        CompletableFuture<Void> voidCompletableFuture;
        voidCompletableFuture = CompletableFuture.runAsync(() -> {
            System.out.println("run " + Thread.currentThread().getName());
        });
        // CompletableFuture, запускающий новый поток, результат которого возьмём у Supplier
        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نمایش داده نمی شود. یعنی هنگام ایجاد یک جریان در جاوا، جریان بلافاصله شروع نمی شود، بلکه منتظر می ماند تا مقداری از آن مورد نیاز باشد. اما CompletableFutureبدون منتظر ماندن برای درخواست مقدار محاسبه شده، زنجیره را برای اجرا شروع می کند. من فکر می کنم درک این موضوع مهم است. بنابراین ما 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 computation
        CompletableFuture.runAsync(task)
                         .thenApply((v) -> longValue.get())
                         .thenApply(dateConverter)
                         .thenAccept(printer);
}
روش ها نسخه هایی thenRunدارند . به این معنی که این مراحل در یک موضوع جدید اجرا می شوند. از یک استخر مخصوص گرفته خواهد شد، بنابراین از قبل معلوم نیست که چه نوع جریانی خواهد بود، جدید یا قدیمی. همه چیز به سختی کارها بستگی دارد. علاوه بر این روش ها، سه احتمال جالب دیگر نیز وجود دارد. برای وضوح، بیایید تصور کنیم که یک سرویس خاص داریم که پیامی را از جایی دریافت می کند و زمان می برد: thenApplythenAcceptAsync
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، ما منتظر نتیجه هستیم. و ما نه تنها می توانیم ترکیب (ترکیب)، بلکه برگردانیم CompletableFuture:
CompletableFuture.completedFuture(2L)
				.thenCompose((val) -> CompletableFuture.completedFuture(val + 2))
                               .thenAccept(result -> System.out.println(result));
در اینجا می خواهم توجه داشته باشم که برای اختصار از روش استفاده شده است CompletableFuture.completedFuture. این روش نخ جدیدی ایجاد نمی کند، بنابراین بقیه زنجیره در همان رشته ای که در آن فراخوانی شده است اجرا می شود completedFuture. یک روش هم وجود دارد thenAcceptBoth. بسیار شبیه به است accept، اما اگر thenAcceptقبول کند consumer، + thenAcceptBothدیگری را به عنوان ورودی می پذیرد ، یعنی 2 منبع را به عنوان ورودی می پذیرد، نه یکی را. یک احتمال جالب دیگر با کلمه وجود دارد : این روش ها جایگزینی را می پذیرند و روی روشی که ابتدا اجرا می شود اجرا می شود. و من می خواهم این بررسی را با یک ویژگی جالب دیگر به پایان برسانم - مدیریت خطا. CompletableStageBiConsumerconsumerEitherشما نمی توانید جاوا را با یک موضوع خراب کنید: قسمت چهارم - Callable، Future and friends - 3CompletableStageCompletableStageCompletableFuture
CompletableFuture.completedFuture(2L)
				 .thenApply((a) -> {
					throw new IllegalStateException("error");
				 }).thenApply((a) -> 3L)
				 //.exceptionally(ex -> 0L)
				 .thenAccept(val -> System.out.println(val));
این کد هیچ کاری انجام نمی دهد، زیرا ... یک استثنا پرتاب می شود و هیچ اتفاقی نمی افتد. اما اگر نظر را لغو کنیم exceptionally، رفتار را تعریف می کنیم. CompletableFutureهمچنین توصیه می کنم ویدیوی زیر را در این زمینه تماشا کنید: به نظر حقیر من، این ویدیوها از بصری ترین ویدیوها در اینترنت هستند. باید از آنها مشخص شود که همه چیز چگونه کار می کند، چه زرادخانه ای داریم و چرا به آن همه نیاز است.

نتیجه

امیدواریم اکنون مشخص شده باشد که چگونه می توان از رشته ها برای بازیابی محاسبات پس از محاسبه آنها استفاده کرد. مواد اضافی: #ویاچسلاو
نظرات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION