Panimula
Sa
Bahagi I , sinuri namin kung paano nilikha ang mga thread. Alalahanin natin ng isang beses.
Ang isang thread ay kinakatawan ng klase ng Thread, kung saan
run()
ang pamamaraan ay tatawagin. Kaya't gamitin natin ang
Tutorialspoint online Java compiler at isagawa ang sumusunod na code:
public class HelloWorld {
public static void main(String[] args) {
Runnable task = () -> {
System.out.println("Hello World");
};
new Thread(task).start();
}
}
Ito ba ang tanging pagpipilian para sa pagsisimula ng isang gawain sa isang thread?
java.util.concurrent.Callable
Lumalabas na ang
java.lang.Runnable ay may kapatid na tinatawag na
java.util.concurrent.Callable na dumating sa mundo sa Java 1.5. Ano ang mga pagkakaiba? Kung titingnan mong mabuti ang Javadoc para sa interface na ito, makikita namin na, hindi katulad ng
Runnable
, ang bagong interface ay nagdedeklara ng isang
call()
paraan na nagbabalik ng resulta. Gayundin, itinapon nito ang Exception bilang default. Ibig sabihin, inililigtas tayo nito mula sa pag-
try-catch
block para sa mga nasuri na exception. Hindi masama, tama? Ngayon ay mayroon na tayong bagong gawain sa halip na
Runnable
:
Callable task = () -> {
return "Hello, World!";
};
Ngunit ano ang gagawin natin dito? Bakit kailangan natin ng gawain na tumatakbo sa isang thread na nagbabalik ng resulta? Malinaw, para sa anumang mga pagkilos na ginawa sa hinaharap, inaasahan naming matatanggap ang resulta ng mga pagkilos na iyon sa hinaharap. At mayroon kaming interface na may katumbas na pangalan:
java.util.concurrent.Future
java.util.concurrent.Future
Ang
java.util.concurrent.Future interface ay tumutukoy sa isang API para sa pagtatrabaho sa mga gawain na ang mga resulta ay pinaplano naming matanggap sa hinaharap: mga paraan upang makakuha ng isang resulta, at mga paraan upang suriin ang katayuan. Tungkol sa
Future
, interesado kami sa pagpapatupad nito sa klase ng
java.util.concurrent.FutureTask . Ito ang "Task" na isasagawa sa
Future
. Ang dahilan kung bakit mas kawili-wili ang pagpapatupad na ito ay nagpapatupad din ito ng Runnable. Maaari mong isaalang-alang ito bilang isang uri ng adaptor sa pagitan ng lumang modelo ng pagtatrabaho sa mga gawain sa mga thread at ang bagong modelo (bago sa kahulugan na ito ay lumitaw sa Java 1.5). Narito ang isang halimbawa:
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());
}
}
Tulad ng makikita mo mula sa halimbawa, ginagamit namin ang
get
paraan upang makuha ang resulta mula sa gawain.
Tandaan:kapag nakuha mo ang resulta gamit ang
get()
pamamaraan, ang pagpapatupad ay nagiging kasabay! Anong mekanismo sa tingin mo ang gagamitin dito? Totoo, walang synchronization block. Iyon ang dahilan kung bakit hindi namin makikita
ang PAGHIHINTAY sa JVisualVM bilang a
monitor
o
wait
, ngunit bilang pamilyar
park()
na pamamaraan (dahil ang
LockSupport
mekanismo ay ginagamit).
Mga functional na interface
Susunod, pag-uusapan natin ang tungkol sa mga klase mula sa Java 1.8, kaya makabubuting magbigay tayo ng maikling panimula. Tingnan ang sumusunod na code:
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);
}
};
Napakaraming dagdag na code, hindi mo ba sasabihin? Ang bawat isa sa mga ipinahayag na klase ay gumaganap ng isang function, ngunit gumagamit kami ng isang grupo ng mga karagdagang sumusuportang code upang tukuyin ito. At ito ay kung paano naisip ng mga developer ng Java. Alinsunod dito, ipinakilala nila ang isang hanay ng mga "functional na interface" (
@FunctionalInterface
) at nagpasya na ngayon ang Java mismo ang gagawa ng "pag-iisip", na iniiwan lamang ang mahahalagang bagay na dapat nating alalahanin:
Supplier<String> supplier = () -> "String";
Consumer<String> consumer = s -> System.out.println(s);
Function<String, Integer> converter = s -> Integer.valueOf(s);
Isang
Supplier
supply. Wala itong mga parameter, ngunit nagbabalik ito ng isang bagay. Ito ay kung paano ito nagbibigay ng mga bagay.
Consumer
Kumokonsumo si A. Ito ay tumatagal ng isang bagay bilang isang input (isang argumento) at may ginagawa dito. Ang argumento ay kung ano ang kinakain nito. Tapos meron din kaming
Function
. Ito ay nangangailangan ng mga input (mga argumento), gumagawa ng isang bagay, at nagbabalik ng isang bagay. Makikita mo na aktibo kaming gumagamit ng mga generic. Kung hindi ka sigurado, maaari kang makakuha ng refresher sa pamamagitan ng pagbabasa ng "
Generics in Java: how to use angled brackets in practice ".
CompletableFuture
Lumipas ang oras at lumitaw ang isang bagong klase na tinawag
CompletableFuture
sa Java 1.8. Ipinapatupad nito ang
Future
interface, ibig sabihin, ang aming mga gawain ay makukumpleto sa hinaharap, at maaari kaming tumawag
get()
upang makuha ang resulta. Ngunit ipinapatupad din nito ang
CompletionStage
interface. Sinasabi ng pangalan ang lahat: ito ay isang tiyak na yugto ng ilang hanay ng mga kalkulasyon. Ang isang maikling pagpapakilala sa paksa ay matatagpuan sa pagsusuri dito: Panimula sa CompletionStage at CompletableFuture. Dumako tayo sa punto. Tingnan natin ang listahan ng mga available na static na pamamaraan na tutulong sa atin na makapagsimula:
Narito ang mga opsyon para sa paggamit ng mga ito:
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";
});
}
}
Kung ipapatupad natin ang code na ito, makikita natin na ang paggawa ng isang
CompletableFuture
ay nagsasangkot din ng paglulunsad ng isang buong pipeline. Samakatuwid, na may tiyak na pagkakatulad sa SteamAPI mula sa Java8, dito natin makikita ang pagkakaiba sa pagitan ng mga pamamaraang ito. Halimbawa:
List<String> array = Arrays.asList("one", "two");
Stream<String> stringStream = array.stream().map(value -> {
System.out.println("Executed");
return value.toUpperCase();
});
Ito ay isang halimbawa ng Stream API ng Java 8. Kung patakbuhin mo ang code na ito, makikita mong hindi ipapakita ang "Isinagawa". Sa madaling salita, kapag ang isang stream ay ginawa sa Java, ang stream ay hindi agad magsisimula. Sa halip, ito ay naghihintay para sa isang tao na gusto ng isang halaga mula dito. Ngunit
CompletableFuture
sinimulan agad na isagawa ang pipeline, nang hindi naghihintay na may humingi nito ng halaga. Sa tingin ko ito ay mahalaga upang maunawaan. O, meron tayong
CompletableFuture
. Paano tayo makakagawa ng pipeline (o chain) at anong mga mekanismo ang mayroon tayo? Alalahanin ang mga functional na interface na isinulat namin kanina.
- Mayroon kaming isang
Function
na tumatagal ng isang A at nagbabalik ng isang B. Ito ay may isang solong pamamaraan: apply()
.
- Mayroon kaming isang
Consumer
kumukuha ng A at walang ibinabalik (Void). Ito ay may iisang paraan: accept()
.
- Mayroon kaming
Runnable
, na tumatakbo sa thread, at walang kinukuha at walang ibinabalik. Ito ay may iisang paraan: run()
.
Ang susunod na dapat tandaan ay ang
CompletableFuture
paggamit ng
Runnable
,
Consumers
, at
Functions
sa gawain nito. Alinsunod dito, maaari mong palaging malaman na magagawa mo ang sumusunod sa
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);
}
Ang
thenRun()
,
thenApply()
, at
thenAccept()
mga pamamaraan ay may mga bersyong "Async." Nangangahulugan ito na ang mga yugtong ito ay makukumpleto sa ibang thread. Ang thread na ito ay kukunin mula sa isang espesyal na pool — kaya hindi natin malalaman nang maaga kung ito ay bago o lumang thread. Ang lahat ay depende sa kung gaano computationally-intensive ang mga gawain. Bilang karagdagan sa mga pamamaraang ito, mayroong tatlong higit pang mga kagiliw-giliw na posibilidad. Para sa kalinawan, isipin natin na mayroon tayong partikular na serbisyo na tumatanggap ng ilang uri ng mensahe mula sa kung saan — at nangangailangan ito ng oras:
public static class NewsService {
public static String getMessage() {
try {
Thread.currentThread().sleep(3000);
return "Message";
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
Ngayon, tingnan natin ang iba pang mga kakayahan na
CompletableFuture
nagbibigay. Maaari nating pagsamahin ang resulta ng a
CompletableFuture
sa resulta ng isa pa
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();
Tandaan na ang mga thread ay mga daemon thread bilang default, kaya para sa kalinawan, ginagamit namin
get()
upang maghintay para sa resulta. Hindi lamang tayo maaaring pagsamahin
CompletableFutures
, maaari rin tayong magbalik ng isang
CompletableFuture
:
CompletableFuture.completedFuture(2L)
.thenCompose((val) -> CompletableFuture.completedFuture(val + 2))
.thenAccept(result -> System.out.println(result));
Dito nais kong tandaan na ang
CompletableFuture.completedFuture()
pamamaraan ay ginamit para sa kaiklian. Ang pamamaraang ito ay hindi gumagawa ng bagong thread, kaya ang natitirang pipeline ay isasagawa sa parehong thread kung saan
completedFuture
tinawag. Mayroon ding
thenAcceptBoth()
pamamaraan. Ito ay halos kapareho sa
accept()
, ngunit kung
thenAccept()
tumatanggap ng
Consumer
,
thenAcceptBoth()
tumatanggap ng isa pang
CompletableStage
+
BiConsumer
bilang input, ibig sabihin, a
consumer
na kumukuha ng 2 pinagmumulan sa halip na isa. May isa pang kawili-wiling kakayahan na inaalok ng mga pamamaraan na ang pangalan ay kinabibilangan ng salitang "Alinman":
Ang mga pamamaraang ito ay tumatanggap ng alternatibo
CompletableStage
at isinasagawa sa kung
CompletableStage
saan ay unang isasagawa. Sa wakas, gusto kong tapusin ang pagsusuri na ito sa isa pang kawili-wiling tampok ng
CompletableFuture
: paghawak ng error.
CompletableFuture.completedFuture(2L)
.thenApply((a) -> {
throw new IllegalStateException("error");
}).thenApply((a) -> 3L)
//.exceptionally(ex -> 0L)
.thenAccept(val -> System.out.println(val));
Walang gagawin ang code na ito, dahil magkakaroon ng exception at walang ibang mangyayari. Ngunit sa pamamagitan ng pag-uncomment sa "katangi-tangi" na pahayag, tinutukoy namin ang inaasahang pag-uugali. Speaking of
CompletableFuture
, inirerekomenda ko rin na panoorin mo ang sumusunod na video:
Sa aking mapagpakumbabang opinyon, ito ay kabilang sa mga pinakanagpapaliwanag na mga video sa Internet. Dapat nilang linawin kung paano gumagana ang lahat ng ito, anong toolkit ang mayroon kami, at kung bakit kailangan ang lahat ng ito.
Konklusyon
Sana, malinaw na ngayon kung paano mo magagamit ang mga thread para makakuha ng mga kalkulasyon pagkatapos makumpleto ang mga ito. Karagdagang materyal:
Mas mahusay na magkasama: Java at ang klase ng Thread. Bahagi I — Mga Thread ng execution Mas mahusay na magkasama: Java at ang Thread class. Bahagi II — Pag-synchronize Mas mahusay na magkasama: Java at ang Thread na klase. Part III — Mas Mahusay na Pakikipag-ugnayan: Java at ang Thread class. Part V — Executor, ThreadPool, Fork/Join Better together: Java at ang Thread class. Bahagi VI - Sunog!
GO TO FULL VERSION