การแนะนำ
ใน
ส่วนที่ 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 — ยิงออกไป!
GO TO FULL VERSION