CodeGym /จาวาบล็อก /สุ่ม /ดีกว่ากัน: Java และคลาสเธรด ส่วนที่สี่ — Callable อนาคต แ...
John Squirrels
ระดับ
San Francisco

ดีกว่ากัน: Java และคลาสเธรด ส่วนที่สี่ — Callable อนาคต และผองเพื่อน

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

การแนะนำ

ในส่วนที่ 1เราได้ตรวจสอบวิธีการสร้างเธรด มารำลึกความหลังกันอีกครั้ง ดีกว่ากัน: Java และคลาสเธรด  ตอนที่ IV — Callable, Future และผองเพื่อน - 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()เมธอด การดำเนินการจะกลายเป็นแบบซิงโครนัส! คุณคิดว่าจะใช้กลไกอะไรที่นี่? จริงไม่มีบล็อกการซิงโครไนซ์ นั่นเป็นเหตุผลที่เราจะไม่เห็นWAITINGใน JVisualVM เป็น a 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บริโภค. ใช้บางอย่างเป็นอินพุต (อาร์กิวเมนต์) และทำบางอย่างกับมัน อาร์กิวเมนต์คือสิ่งที่มันกิน แล้วเรายังมีFunction. รับอินพุต (อาร์กิวเมนต์) ทำบางสิ่ง และส่งคืนบางสิ่ง คุณจะเห็นว่าเรากำลังใช้ยาชื่อสามัญอย่างแข็งขัน หากคุณไม่แน่ใจ คุณสามารถทบทวนได้โดยอ่าน " Generics in Java: how to use angled brackets in practice "

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

เวลาผ่านไปและคลาสใหม่ที่เรียกว่าCompletableFutureปรากฏใน Java 1.8 มันใช้Futureงานอินเทอร์เฟซ กล่าวคือ งานของเราจะเสร็จสิ้นในอนาคต และเราสามารถโทรติดต่อget()เพื่อขอทราบผลได้ แต่ยังใช้CompletionStageอินเทอร์เฟซ ชื่อบอกไว้หมดแล้ว: นี่คือขั้นตอนหนึ่งของชุดการคำนวณบางชุด คำแนะนำสั้น ๆ เกี่ยวกับหัวข้อนี้สามารถพบได้ในบทวิจารณ์ที่นี่: บทนำสู่ CompletionStage และ CompletableFuture มาเข้าประเด็นกันเลย มาดูรายการวิธีการแบบสแตติกที่มีอยู่ซึ่งจะช่วยให้เราเริ่มต้นได้: ดีกว่ากัน: Java และคลาสเธรด  ตอนที่ IV — Callable, Future และผองเพื่อน - 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";
        });
    }
}
หากเรารันโค้ดนี้ เราจะเห็นว่าการสร้าง a 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. เราจะสร้างไปป์ไลน์ (หรือโซ่) ได้อย่างไร และเรามีกลไกอะไรบ้าง? เรียกคืนอินเทอร์เฟซการทำงานที่เราเขียนไว้ก่อนหน้านี้
  • เรามี a Functionที่รับ A และส่งคืน B มันมีเมธอดเดียวapply():
  • เรามี a 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()" Async" ซึ่งหมายความว่าขั้นตอนเหล่านี้จะเสร็จสิ้นในเธรดอื่น เธรดนี้จะนำมาจากกลุ่มพิเศษ ดังนั้นเราจะไม่ทราบล่วงหน้าว่าจะเป็นเธรดใหม่หรือเก่า ทุกอย่างขึ้นอยู่กับว่างานนั้นใช้คอมพิวเตอร์มากเพียงใด นอกจากวิธีการเหล่านี้แล้ว ยังมีความเป็นไปได้ที่น่าสนใจอีกสามวิธี เพื่อความชัดเจน ลองจินตนาการว่าเรามีบริการบางอย่างที่ได้รับข้อความบางประเภทจากที่ไหนสักแห่ง — ซึ่งต้องใช้เวลา: 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เรายังสามารถส่งคืน a 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()ยอมรับอีกCompletableStage+ BiConsumerเป็นอินพุต นั่นคือ a consumerที่รับ 2 แหล่งแทนที่จะเป็น 1 แหล่ง มีความสามารถที่น่าสนใจอีกอย่างที่เสนอโดยเมธอดที่มีคำว่า "Either" รวมอยู่ด้วย: ดีกว่ากัน: Java และคลาสเธรด  ตอนที่ IV — Callable, Future และผองเพื่อน - 3เมธอดเหล่านี้ยอมรับทางเลือกอื่นCompletableStageและดำเนินการในอันCompletableStageที่จะถูกเรียกใช้งานก่อน สุดท้ายนี้ ผมขอจบการรีวิวนี้ด้วยคุณสมบัติที่น่าสนใจอีกอย่างของCompletableFuture: การจัดการข้อผิดพลาด

CompletableFuture.completedFuture(2L)
				 .thenApply((a) -> {
					throw new IllegalStateException("error");
				 }).thenApply((a) -> 3L)
				 //.exceptionally(ex -> 0L)
				 .thenAccept(val -> System.out.println(val));
รหัสนี้จะไม่ทำอะไรเลย เพราะจะมีข้อยกเว้นและจะไม่มีอะไรเกิดขึ้นอีก แต่โดยการไม่แสดงความคิดเห็นคำสั่ง "พิเศษ" เราจะกำหนดลักษณะการทำงานที่คาดหวัง เมื่อพูดถึงCompletableFutureฉันขอแนะนำให้คุณดูวิดีโอต่อไปนี้: ในความเห็นอันต่ำต้อยของฉัน วิดีโอเหล่านี้เป็นหนึ่งในวิดีโอที่มีคำอธิบายมากที่สุดบนอินเทอร์เน็ต พวกเขาควรทำให้ชัดเจนว่าทั้งหมดนี้ทำงานอย่างไร ชุดเครื่องมือใดที่เรามีให้ และเหตุใดจึงจำเป็นต้องใช้ทั้งหมดนี้

บทสรุป

หวังว่าตอนนี้จะชัดเจนว่าคุณสามารถใช้เธรดเพื่อรับการคำนวณได้อย่างไรหลังจากทำเสร็จแล้ว วัสดุเพิ่มเติม: ดีกว่ากัน: Java และคลาสเธรด ส่วนที่ 1 — เธรดของการดำเนินการ ดีขึ้นด้วยกัน: Java และคลาสเธรด ส่วนที่ II — การซิงโครไนซ์ เข้าด้วยกันได้ดีขึ้น: Java และคลาสเธรด ส่วนที่ 3 — ปฏิสัมพันธ์ ร่วมกันได้ดีขึ้น: Java และคลาสเธรด ส่วนที่ V — Executor, ThreadPool, Fork/Jin Better together: Java และคลาส Thread ตอนที่ VI — ยิงออกไป!
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION