JavaRush /จาวาบล็อก /Random-TH /คุณไม่สามารถทำลาย Java ด้วยเธรด: ตอนที่ 4 - Callable, Fut...
Viacheslav
ระดับ

คุณไม่สามารถทำลาย Java ด้วยเธรด: ตอนที่ 4 - Callable, Future และผองเพื่อน

เผยแพร่ในกลุ่ม

การแนะนำ

เราได้ดูวิธีการสร้างเธรด ใน ส่วนแรก แล้ว เรามารำลึกกันอีกครั้ง คุณไม่สามารถสปอย Java ด้วยเธรดได้: ตอนที่ IV - Callable, Future และ Friends - 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และเขาเกิดใน Java 1.5 อะไรคือความแตกต่าง? หากเราพิจารณา JavaDoc ของอินเทอร์เฟซนี้ให้ละเอียดยิ่งขึ้น เราจะเห็นว่าRunnableอินเทอร์เฟซใหม่ประกาศวิธีการcallส่งคืนผลลัพธ์ ซึ่ง ต่างจาก ตรงที่ นอกจากนี้ ตามค่าเริ่มต้น มันจะส่งข้อยกเว้น นั่นคือช่วยให้เราไม่ต้องเขียนtry-catchบล็อกสำหรับข้อยกเว้นที่ตรวจสอบแล้ว ก็ไม่เลวอยู่แล้วใช่ไหม? ตอนนี้เรามีRunnableงานใหม่แทน:
Callable task = () -> {
	return "Hello, World!";
};
แต่จะทำอย่างไรกับมัน? เหตุใดเราจึงต้องมีงานที่ทำงานบนเธรดที่ส่งคืนผลลัพธ์? แน่นอนว่าในอนาคตเราคาดว่าจะได้รับผลของการกระทำที่จะเกิดขึ้นในอนาคต อนาคตในภาษาอังกฤษ - อนาคต และมีอินเทอร์เฟซที่มีชื่อเหมือนกันทุกประการ:java.util.concurrent.Future

java.util.concurrent.อนาคต

อินเทอร์เฟซ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การดำเนินการจะกลายเป็นแบบซิงโครนัส คุณคิดว่าจะใช้กลไกอะไรที่นี่? ถูกต้อง ไม่มีบล็อกการซิงโครไนซ์ - ดังนั้น เราจะเห็น WAITINGใน 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) และทำอะไรบางอย่างกับมัน นั่นคือ กินอะไรบางอย่าง มีฟังก์ชั่นอื่น ใช้บางสิ่งเป็นอินพุต (พารามิเตอร์s) ทำบางสิ่งและส่งคืนบางสิ่ง ดังที่เราเห็นมีการใช้ยาชื่อสามัญอย่างแข็งขัน หากคุณไม่แน่ใจ คุณสามารถจำไว้และอ่าน “ ทฤษฎีของยาชื่อสามัญในภาษา Java หรือวิธีใส่วงเล็บในทางปฏิบัติ

อนาคตที่สมบูรณ์

เมื่อเวลาผ่านไป Java 1.8 ได้เปิดตัวคลาสใหม่ที่เรียกว่าCompletableFuture. มันใช้อินเทอร์เฟซFutureซึ่งหมายความว่าของเราtaskจะถูกดำเนินการในอนาคตและเราสามารถดำเนินการgetและรับผลลัพธ์ได้ แต่เขาก็ดำเนินการบางอย่างCompletionStageด้วย จากการแปลวัตถุประสงค์ก็ชัดเจนแล้ว: เป็นขั้นตอนหนึ่งของการคำนวณบางประเภท ข้อมูลเบื้องต้นเกี่ยวกับหัวข้อนี้โดยย่อสามารถพบได้ในภาพรวม " บทนำสู่ขั้นตอนที่เสร็จสมบูรณ์และอนาคตที่สมบูรณ์ " มาตรงประเด็นกันดีกว่า มาดูรายการวิธีการคงที่ที่มีอยู่เพื่อช่วยเราในการเริ่มต้น: คุณไม่สามารถสปอย Java ด้วยเธรด: ตอนที่ IV - Callable, Future และ Friends - 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บ่งบอกถึงการเปิดตัวของ chain ทั้งหมด ดังนั้น แม้ว่า 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จะเริ่มเชนเพื่อดำเนินการทันที โดยไม่ต้องรอให้ถามถึงค่าที่คำนวณได้ ฉันคิดว่าสิ่งสำคัญคือต้องเข้าใจสิ่งนี้ ดังนั้นเราจึงมี CompletableFuture เราจะสร้างห่วงโซ่ได้อย่างไร และเรามีวิธีการอย่างไร? จำเกี่ยวกับอินเทอร์เฟซการทำงานที่เราเขียนไว้ก่อนหน้านี้
  • เรามีฟังก์ชัน ( Function) ที่รับ A และส่งกลับ B โดยมีวิธีการเดียวคือ - 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มี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();
เป็นที่น่าสังเกตว่าตามค่าเริ่มต้น เธรดจะเป็นเธรด daemon ดังนั้นเพื่อความชัดเจนgetเราจึงใช้เพื่อรอผลลัพธ์ และเราไม่เพียงแต่สามารถรวม (รวม) เท่านั้น แต่ยังส่งคืนCompletableFuture:
CompletableFuture.completedFuture(2L)
				.thenCompose((val) -> CompletableFuture.completedFuture(val + 2))
                               .thenAccept(result -> System.out.println(result));
ในที่นี้ฉันอยากจะทราบว่าเพื่อความกระชับ มีการใช้วิธีCompletableFuture.completedFutureนี้ เมธอดนี้ไม่ได้สร้างเธรดใหม่ ดังนั้นเชนที่เหลือจะถูกดำเนินการในเธรดเดียวกันกับที่ถูกเรียกcompletedFutureว่า นอกจากนี้ยังมีวิธีthenAcceptBothการ มันคล้ายกันมากกับacceptแต่ถ้าthenAcceptยอมรับมันก็จะยอมรับอีก+ เป็นอินพุต consumerนั่นคือซึ่งยอมรับ 2 แหล่งเป็นอินพุต ไม่ใช่แหล่งเดียว มีความเป็นไปได้ที่น่าสนใจอีกประการหนึ่งสำหรับคำว่า: วิธีการเหล่านี้ยอมรับทางเลือกอื่นและจะดำเนินการกับวิธีที่ถูกดำเนินการก่อน และฉันอยากจะจบการรีวิวนี้ด้วยฟีเจอร์ที่น่าสนใจอีกอย่างหนึ่งนั่นก็คือ การจัดการข้อผิดพลาด thenAcceptBothCompletableStageBiConsumerconsumerEitherคุณไม่สามารถทำลาย Java ด้วยเธรดได้: ตอนที่ IV - Callable, Future และ 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