9 misstag att undvika när du programmerar i Java
Vilka misstag bör man undvika när man programmerar i Java? I följande stycke besvarar vi denna fråga.
Läs den första delen av vår bloggserie om samtidighet i Java. I följande artikel kommer vi att titta närmare på skillnader mellan trådar och processer, trådpooler, exekutorer och mycket mer!
I allmänhet är den konventionella programmeringsmetoden sekventiell. Allt i ett program sker ett steg i taget.
Men i själva verket är det parallellt som hela världen fungerar - det är förmågan att utföra mer än en uppgift samtidigt.
För att diskutera avancerade ämnen som samtidighet i Java eller multithreading måste vi enas om några gemensamma definitioner för att vara säkra på att vi är på samma sida.
Låt oss börja med grunderna. I den icke-sekventiella världen har vi två typer av samtidighetsrepresentanter: processer och
trådar. En process är en instans av det program som körs. Normalt sett är den isolerad från andra processer.
Operativsystemet ansvarar för att tilldela resurser till varje process. Dessutom fungerar det som en ledare som
schemalägger och kontrollerar dem.
Thread är en slags process men på en lägre nivå, därför kallas den också för light thread. Flera trådar kan köras i en
process. Här fungerar programmet som en schemaläggare och en styrenhet för trådar. På så sätt verkar enskilda program göra
flera uppgifter på samma gång.
Den grundläggande skillnaden mellan trådar och processer är isoleringsnivån. Processen har sin egen uppsättning av
resurser, medan tråden delar data med andra trådar. Det kan tyckas vara en felbenägen metod och det är det verkligen. För
men låt oss inte fokusera på det eftersom det ligger utanför ramen för den här artikeln.
Processer, trådar - okej... Men vad är egentligen samtidighet? Concurrency innebär att du kan utföra flera uppgifter samtidigt
tid. Det betyder inte att dessa uppgifter måste köras samtidigt - det är det som är parallellism. Concurrenc i Javay gör inte heller
kräver att du har flera processorer eller till och med flera kärnor. Det kan uppnås i en enkärnig miljö genom att utnyttja
byte av sammanhang.
En term som är relaterad till concurrency är multithreading. Detta är en funktion i program som gör att de kan utföra flera uppgifter samtidigt. Det är inte alla program som använder detta tillvägagångssätt, men de som gör det kan kallas flertrådade.
Vi är nästan klara, bara en definition till. Asynkroni innebär att ett program utför icke-blockerande operationer.
Den initierar en uppgift och fortsätter sedan med andra saker i väntan på responsen. När den får svaret kan den reagera på det.
Som standard är varje Java-applikation körs i en process. I den processen finns det en tråd som är relaterad till main()
metod för
en applikation. Som nämnts är det dock möjligt att utnyttja mekanismerna för flera trådar inom en
program.
Tråd
är en Java klass där magin sker. Detta är objektrepresentationen av den tidigare nämnda tråden. Till
skapa din egen tråd kan du utöka Tråd
klass. Det är dock inte ett rekommenderat tillvägagångssätt. Trådar
bör användas som en mekanism för att köra uppgiften. Uppgifter är delar av kod som vi vill köra i ett parallellt läge. Vi kan definiera dem med hjälp av Kan köras
gränssnitt.
Men nog med teori, låt oss sätta vår kod där vår mun är.
Anta att vi har ett par matriser med siffror. För varje matris vill vi veta summan av siffrorna i en matris. Låt oss
Låtsas att det finns många sådana matriser och att var och en av dem är relativt stor. Under sådana förhållanden vill vi utnyttja samtidighet och summera varje matris som en separat uppgift.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
Körbar uppgift1 = () -> {
int sum = Arrays.stream(a1).sum();
System.out.println("1. Summan är: " + sum);
};
Körbar uppgift2 = () -> {
int sum = Arrays.stream(a2).sum();
System.out.println("2. Summan är: " + sum);
};
Körbar uppgift3 = () -> {
int sum = Arrays.stream(a3).sum();
System.out.println("3. Summan är: " + sum);
};
ny tråd(uppgift1).start();
ny tråd(uppgift2).start();
ny tråd(uppgift3).start();
Som du kan se från koden ovan Kan köras
är ett funktionellt gränssnitt. Det innehåller en enda abstrakt metod kör()
utan några argument. Den Kan köras
gränssnitt bör implementeras av alla klasser vars instanser är avsedda att vara
som utförs av en tråd.
När du har definierat en uppgift kan du skapa en tråd för att köra den. Detta kan göras via ny tråd()
konstruktör som
tar Kan köras
som sitt argument.
Det sista steget är att start()
en nyupprättad tråd. I API:et finns också kör()
metoder i Kan köras
och iTråd
. Detta är dock inte ett sätt att utnyttja samtidighet i Java. Ett direkt anrop till var och en av dessa metoder resulterar i
utföra uppgiften i samma tråd som den main()
metoden körs.
När det finns många uppgifter är det inte en bra idé att skapa en separat tråd för varje uppgift. Att skapa en Tråd
är en
tungviktsoperation och det är mycket bättre att återanvända befintliga trådar än att skapa nya.
När ett program skapar många kortlivade trådar är det bättre att använda en trådpool. Trådpoolen innehåller ett antal
trådar som är redo att köras men som för närvarande inte är aktiva. Att ge en Kan köras
till poolen gör att en av trådarna anropar funktionenkör()
metod för given Kan köras
. Efter att ha slutfört en uppgift finns tråden fortfarande kvar och är i ett viloläge.
Okej, du fattar - föredra trådpooler istället för manuell skapelse. Men hur kan du använda dig av trådpooler? Den Utförare
klassen har ett antal statiska fabriksmetoder för att konstruera trådpooler. Till exempel newCachedThredPool()
skapar
en pool där nya trådar skapas efter behov och inaktiva trådar sparas i 60 sekunder. I motsats till detta,newFixedThreadPool()
innehåller en fast uppsättning trådar, där inaktiva trådar förvaras på obestämd tid.
Låt oss se hur det kan fungera i vårt exempel. Nu behöver vi inte skapa trådar manuellt. Istället måste vi skapaUtförarService
som tillhandahåller en pool av trådar. Sedan kan vi tilldela uppgifter till den. Det sista steget är att stänga tråden
pool för att undvika minnesläckage. Resten av den tidigare koden förblir densamma.
ExecutorService executor = Executors.newCachedThreadPool();
executor.submit(uppgift1);
executor.submit(uppgift2);
executor.submit(uppgift3);
exekutör.shutdown();
Kan köras
verkar vara ett smidigt sätt att skapa samtidiga uppgifter, men det har en stor brist. Den kan inte returnera någon
värde. Dessutom kan vi inte avgöra om en uppgift är slutförd eller inte. Vi vet inte heller om den har slutförts
normalt eller exceptionellt. Lösningen på dessa problem är Kallbara
.
Kallbara
liknar Kan köras
på sätt och vis även asynkrona uppgifter. Den största skillnaden är att den kan
returnera ett värde. Returvärdet kan vara av valfri (icke-primitiv) typ som Kallbara
gränssnittet är en parameteriserad typ.Kallbara
är ett funktionellt gränssnitt som har anrop()
metod som kan ge en Undantag
.
Låt oss nu se hur vi kan utnyttja Kallbara
i vårt matrisproblem.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
Callable task1 = () -> Arrays.stream(a1).sum();
Callable task2 = () -> Arrays.stream(a2).sum();
Callable task3 = () -> Arrays.stream(a3).sum();
ExecutorService executor = Executors.newCachedThreadPool();
Future future1 = executor.submit(task1);
Future future2 = executor.submit(task2);
Future future3 = executor.submit(task3);
System.out.println("1. Summan är: " + future1.get());
System.out.println("2. Summan är: " + future2.get());
System.out.println("3. Summan är: " + future3.get());
Executor.shutdown();
Okej, vi kan se hur Kallbara
skapas och sedan skickas till UtförarService
. Men vad i hela friden är Framtid
?Framtid
fungerar som en brygga mellan trådar. Summan av varje array produceras i en separat tråd och vi behöver ett sätt att
få tillbaka dessa resultat till main()
.
För att hämta resultatet från Framtid
vi behöver ringa get()
metod. Här kan en av två saker hända. Först kommer
resultatet av beräkningen som utförs av Kallbara
är tillgänglig. Då får vi det omedelbart. För det andra är resultatet inte
redo ännu. I det fallet get()
metoden blockeras tills resultatet är tillgängligt.
Problemet med Framtid
är att den fungerar enligt "push-paradigmet". När man använder Framtid
måste du vara som en chef som
frågar ständigt: "Är din uppgift klar? Är den klar?" tills den ger ett resultat. Att agera under konstant press är
dyrt. Mycket bättre tillvägagångssätt skulle vara att beställa Framtid
vad den ska göra när den är klar med sin uppgift. Tyvärr är det så,Framtid
kan inte göra det men ComputableFuture
kan.
ComputableFuture
arbetar enligt "pull-paradigmet". Vi kan tala om för den vad den ska göra med resultatet när den har slutfört sina uppgifter. Den
är ett exempel på ett asynkront tillvägagångssätt.
ComputableFuture
fungerar perfekt med Kan köras
men inte med Kallbara
. Istället är det möjligt att ge en uppgift tillComputableFuture
i form av Leverantör
.
Låt oss se hur ovanstående förhåller sig till vårt problem.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
CompletableFuture.supplyAsync(() -> Arrays.stream(a1).sum())
.thenAccept(System.out::println);
CompletableFuture.supplyAsync(() -> Arrays.stream(a2).sum())
.thenAccept(System.out::println);
CompletableFuture.supplyAsync(() -> Arrays.stream(a3).sum())
.thenAccept(System.out::println);
Det första som slår en är hur mycket kortare den här lösningen är. Dessutom ser det snyggt och prydligt ut.
Uppgift till Kompletterbar framtid
kan tillhandahållas av supplyAsync()
metod som tar Leverantör
eller av runAsync()
att
tar Kan köras
. En callback - en del av koden som ska köras när en uppgift är slutförd - definieras av thenAccept()
metod.
Java erbjuder många olika sätt att hantera samtidighet. I den här artikeln har vi knappt berört ämnet.
Men vi har ändå gått igenom grunderna i Tråd
, Kan köras
, Kallbara
, och KallbarFutur
vilket är en bra poäng
för vidare undersökning av ämnet.