- Bir iş parçacığı oluşturmak,
start()ile başlatmak vejoin()ile beklemek - Bir yarış durumunu tanımak ve onu
synchronizedya daAtomicIntegerile gidermek - Görevleri
ExecutorService,CompletableFutureve sanal iş parçacıklarıyla çalıştırmak
Tek kasası olan bir markette kuyruk uzar, beş kasa açılınca ise müşterilere hızla hizmet verilir. Bilgisayarın işlemcisinde de birkaç çekirdek vardır ve bir sunucu aynı anda binlerce isteğe yanıt vermek zorundadır. Birden çok işi aynı anda yapabilme yeteneğine eşzamanlılık (concurrency) denir. Java bu alanda en güçlü dillerden biridir: basit iş parçacıklarından Java 21'in sanal iş parçacıklarına kadar.
Bir programın içindeki bağımsız bir yürütme hattı. Her Java programı main adlı tek bir iş parçacığıyla başlar; yeni iş parçacıkları oluşturup işi aralarında bölüştürebilirsin. İş parçacıkları aynı belleği, yani aynı nesneleri paylaşır; bu hem kullanışlı hem de tehlikelidir.
İş parçacığı oluşturmak: start ve join
Yapılacak işi new Thread(...) yapıcısına bir lambda olarak veriyoruz. start() yeni iş parçacığını başlatır ve hemen geri döner; main kendi işine devam eder. join() ise o iş parçacığı bitene kadar bekler. Aşağıda 1'den 1.000.000'a kadar sayıların toplamı iki yarıya bölündü ve her yarıyı ayrı bir iş parçacığı hesaplıyor; sonuçları ancak join()'den sonra okuyoruz.
public class Main {
public static void main(String[] args) throws InterruptedException {
long[] sums = new long[2];
Thread first = new Thread(() -> sums[0] = sum(1, 500_000));
Thread second = new Thread(() -> sums[1] = sum(500_001, 1_000_000));
first.start();
second.start();
first.join();
second.join();
System.out.println("Part 1: " + sums[0]);
System.out.println("Part 2: " + sums[1]);
System.out.println("Total: " + (sums[0] + sums[1]));
System.out.println("Printed by: " + Thread.currentThread().getName());
}
static long sum(long from, long to) {
long total = 0;
for (long i = from; i <= to; i++) total += i;
return total;
}
}Part 1: 125000250000 Part 2: 375000250000 Total: 500000500000 Printed by: main
main ise onları join() ile beklerThread t = new Thread(() -> System.out.println(Thread.currentThread().getName()));
t.run(); // prints main: no new thread was created!Thread t = new Thread(() -> System.out.println(Thread.currentThread().getName()));
t.start(); // prints Thread-0: the code runs in a new thread
t.join();Paylaşılan veri: yarış durumu
count++ tek bir işlem gibi görünür ama aslında üç adımdır: değeri oku, 1 ekle, geri yaz. İki iş parçacığı aynı anda okursa ikisi de aynı değeri yazar ve bir artırma kaybolur. Bu hataya yarış durumu (race condition) denir: sonuç, iş parçacıklarının rastgele sırasına bağlıdır. Bir denemede 100.000'lik döngüler çalıştıran iki iş parçacığı 200.000 yerine örneğin 121.962 verdi; her çalıştırmada da farklı bir sayı çıkar.
static int count = 0;
// each of two threads runs:
for (int i = 0; i < 100_000; i++) {
count++; // read + add + write: not atomic
}
// result: often less than 200000static final AtomicInteger count = new AtomicInteger();
// each of two threads runs:
for (int i = 0; i < 100_000; i++) {
count.incrementAndGet(); // one indivisible operation
}
// result: always 200000Sorunu çözmenin iki ana yolu var. synchronized anahtar sözcüğü metodu kilitler: içinde aynı anda yalnızca bir iş parçacığı bulunabilir, diğerleri sırasını bekler. java.util.concurrent.atomic paketindeki AtomicInteger, AtomicLong gibi sınıflar ise artırmayı kilitsiz, bölünmez tek bir işlemci işlemiyle yapar ve genellikle daha hızlıdır. Koleksiyonlar için hazır güvenli sürümler vardır: ConcurrentHashMap, CopyOnWriteArrayList.
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
static int syncCount = 0;
static final AtomicInteger atomicCount = new AtomicInteger();
static synchronized void incrementSync() {
syncCount++;
}
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 100_000; i++) {
incrementSync();
atomicCount.incrementAndGet();
}
};
Thread a = new Thread(task);
Thread b = new Thread(task);
a.start();
b.start();
a.join();
b.join();
System.out.println("synchronized: " + syncCount);
System.out.println("AtomicInteger: " + atomicCount.get());
}
}synchronized: 200000 AtomicInteger: 200000
ExecutorService: iş parçacığı havuzu
Her görev için yeni bir iş parçacığı oluşturmak pahalıdır. ExecutorService, hazır iş parçacıklarından oluşan bir havuz (pool) tutar ve görevleri onlara dağıtır. submit tek bir görev verir ve bir Future döndürür; yani gelecekteki sonucun “makbuzunu”. invokeAll ise bir görev listesi verir ve hepsini bekler. Future.get() sonuç hazır olana kadar bekler. Java 19'dan beri bir ExecutorService, try-with-resources ile kapatılabilir: blok bittiğinde tüm görevler beklenir ve havuz kapanır.
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) throws Exception {
List<String> classes = List.of("10A", "10B", "11A");
long start = System.currentTimeMillis();
try (ExecutorService pool = Executors.newFixedThreadPool(3)) {
List<Callable<String>> jobs = classes.stream()
.map(c -> (Callable<String>) () -> buildReport(c))
.toList();
for (Future<String> f : pool.invokeAll(jobs)) {
System.out.println(f.get());
}
}
long ms = System.currentTimeMillis() - start;
System.out.println("Parallel was faster: " + (ms < 900));
}
static String buildReport(String name) throws InterruptedException {
Thread.sleep(300); // pretend to read the database
return "Report for " + name + " is ready";
}
}Report for 10A is ready Report for 10B is ready Report for 11A is ready Parallel was faster: true
CompletableFuture: asenkron zincirler
Future.get() iş parçacığını engeller. CompletableFuture ise sonuç geldiğinde ne yapılacağını önceden tarif etmeni sağlar: supplyAsync işi arka planda başlatır, thenApply sonucu dönüştürür, thenCombine iki bağımsız sonucu birleştirir, exceptionally ise bir hata olursa yedek bir değer verir. Sonunda join() nihai sonucu alır. Bu, JavaScript'teki Promise'e çok benzer.
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) {
CompletableFuture<Integer> exam = CompletableFuture.supplyAsync(() -> slow(92, 300));
CompletableFuture<Integer> bonus = CompletableFuture.supplyAsync(() -> slow(5, 200));
CompletableFuture<String> report = exam
.thenCombine(bonus, Integer::sum)
.thenApply(total -> Math.min(total, 100))
.thenApply(total -> "Final score: " + total);
System.out.println(report.join());
CompletableFuture<Integer> broken = CompletableFuture
.supplyAsync(() -> Integer.parseInt("ninety"))
.exceptionally(ex -> -1);
System.out.println("With fallback: " + broken.join());
}
static int slow(int value, long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return value;
}
}Final score: 97 With fallback: -1
ninety sayıya çevrilemediği için yedek değer -1 devreye girerSanal iş parçacıkları (Java 21)
Sıradan (platform) bir iş parçacığı işletim sisteminin bir iş parçacığına bağlıdır ve yığını için genellikle yaklaşık 1 MB bellek ayırır; bu yüzden binlercesini oluşturmak zordur. Java 21 ile gelen sanal iş parçacıkları ise JVM'nin yönettiği hafif iş parçacıklarıdır: beklerken (ağ, dosya, veritabanı) alttaki iş parçacığını serbest bırakırlar ve milyonlarcası oluşturulabilir. Aşağıda 10.000 görevin her biri 1 saniye “bekliyor”. 100 iş parçacıklı sıradan bir havuzda bu yaklaşık 100 saniye sürerdi; sanal iş parçacıklarıyla ise bir iki saniye sürer.
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
public static void main(String[] args) {
AtomicInteger done = new AtomicInteger();
Instant start = Instant.now();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1)); // a slow network call
done.incrementAndGet();
return null;
});
}
} // close() waits until every task has finished
Duration took = Duration.between(start, Instant.now());
System.out.println("Finished tasks: " + done.get());
System.out.println("Took less than 5 s: " + (took.toSeconds() < 5));
Thread vt = Thread.ofVirtual().start(() -> {});
System.out.println("Is virtual: " + vt.isVirtual());
}
}Finished tasks: 10000 Took less than 5 s: true Is virtual: true
newVirtualThreadPerTaskExecutor()Sanal iş parçacıklarıyla yazılan kod, sıradan ardışık koda benzer: Thread.sleep, dosya okuma ve veritabanı sorgusu her zamanki gibi yazılır; geri çağırmalara ve zincirlere gerek kalmaz. Tek bir iş parçacığını Thread.ofVirtual().start(...) ya da Thread.startVirtualThread(...) ile de başlatabilirsin. Web sunucuları da bundan yararlanır: örneğin Spring Boot 3.2'den beri spring.threads.virtual.enabled=true ayarı her HTTP isteğini sanal bir iş parçacığında işler.
| Durum | Araç |
|---|---|
| Çok bekleme: HTTP istekleri, veritabanları, dosyalar | newVirtualThreadPerTaskExecutor() |
| Ağır hesaplamalar (CPU) | newFixedThreadPool(cores) |
| Sonuçları zincirleme ve birleştirme | CompletableFuture |
| Paylaşılan sayaç ya da eşlem | AtomicInteger, ConcurrentHashMap |
Önemli noktalar
start()yeni bir iş parçacığı açar,join()onu bekler;run()ise iş parçacığı oluşturmaz.- Paylaşılan değişken veri yarış durumuna yol açar; çözümler
synchronized, atomik türler veConcurrentHashMap'tir. ExecutorServiceiş parçacıklarını yeniden kullanır;submitveinvokeAllFuturedöndürür.- Bir
CompletableFuturezinciri:supplyAsyncişi başlatır,thenApply/thenCombinesonuçları dönüştürür ve birleştirir,exceptionallyhataları yakalar,joinise nihai değeri alır. - Java 21'in sanal iş parçacıkları beklemeli işler içindir; hesaplama için çekirdek sayısı kadar iş parçacıklı bir havuz seç.
Kendini test et
10 soru. Her doğru cevap XP kazandırır.
t.start() yerine t.run() çağırırsan ne olur?