Çoklu İş Parçacığı ve Eşzamanlılık
Modern bilgisayarların birden çok çekirdeği var ve gerçek uygulamalar aynı anda birçok iş yapar: bir gösterge paneli onlarca servisten veri toplar, bir e-ticaret sitesi binlerce isteği aynı anda karşılar. Çoklu iş par…
Çoklu İş Parçacığı ve Eşzamanlılık
Modern bilgisayarların birden çok çekirdeği var ve gerçek uygulamalar aynı anda birçok iş yapar: bir gösterge paneli onlarca servisten veri toplar, bir e-ticaret sitesi binlerce isteği aynı anda karşılar. Çoklu iş parçacığı (multithreading) bu paralelliği sağlar; ama paylaşılan veriye aynı anda dokunulduğunda sinsi hatalar doğar. Bu yüzden hem paralelliği kurmayı hem de onu güvenli kılan eşzamanlılık araçlarını öğrenmek gerekir. Bu konu güçlü ama dikkat ister — gerçek senaryolarla adım adım ilerleyeceğiz.
Bu bölümün örnekleri
join/awaitTerminationile thread'leri bekleyerek düzenli ve tekrarlanabilir çıktı üretir.
Process ve thread
Process, çalışan bir programdır ve kendi belleğine sahiptir. Thread (iş parçacığı) ise bir process içindeki bağımsız bir yürütme akışıdır. Aynı process'in thread'leri aynı belleği paylaşır — bu, hızlı veri paylaşımı sağlar ama "iki thread aynı veriyi aynı anda değiştirirse ne olur?" sorusunu da doğurur.
Neden paralellik? Gerçek bir kazanç
Diyelim ki bir gösterge paneli üç ayrı servisten (satış, stok, müşteri) veri çekiyor ve her
biri ~300 ms sürüyor. Sırayla beklersen ~900 ms; üçünü paralel çalıştırırsan toplam süre
en yavaş servis kadar, yani ~300 ms olur. Örnek 1 (./Ornek1.java) bunu canlı ölçer:
servisleri ayrı thread'lerde çalıştırır, sonuçları thread-safe bir ConcurrentHashMap'e yazar
ve join ile hepsinin bitmesini bekler.
Thread oluşturma
İki yol vardır; tercih edilen, işi bir Runnable (genelde lambda) olarak verip Thread'e
geçmektir:
Runnable is = () -> sonuc.put("satis", servisCagir()); Thread t = new Thread(is, "satis-thread"); t.start(); // YENİ bir iş parçacığı başlatır (run() değil!) t.join(); // bu thread bitene kadar BEKLE
start() yeni bir thread'de çalışır; doğrudan run() çağırırsan paralellik olmaz. join(),
"sonuçlar hazır olana dek bekle" demenin yoludur.
Yarış durumu (race condition): en kritik konu
Asıl tehlike burada. Bir konser için 100 bilet var ve 4 satış noktası aynı anda satıyor. "Bilet var mı? Varsa sat" kodu masum görünür ama iki adımdır: önce kontrol, sonra artırma. İki thread aynı anda "var" görüp ikisi de satarsa fazla satış olur — gerçek hayatta felaket.
Örnek 2 (./Ornek2.java) bu hatayı önce korumasız kodla gösterir (yarış durumu yüzünden
çoğu çalıştırmada 100'den fazla bilet satar veya tutarsız bir sayı üretir), sonra üç farklı
yolla düzeltir:
synchronized: Bir metodu/bloğu aynı anda yalnızca tek thread'in çalıştırmasını garantiler (karşılıklı dışlama).synchronized void satisDene() { if (satilan < TOPLAM) satilan++; }ReentrantLock:synchronized'in daha esnek hali;tryLock, zaman aşımı, adil kilit gibi seçenekler sunar.lock()/unlock()'u her zamantry/finallyile kullan.AtomicInteger: Kilit kullanmadan, donanım destekli atomik işlemler (compareAndSet,incrementAndGet). Sayaçlar için hem doğru hem hızlıdır.
Görünürlük (visibility) ve volatile
Race condition'ın az bilinen bir akrabası görünürlük sorunudur: bir thread'in yazdığı
değeri başka bir thread, önbellekleme yüzünden hemen görmeyebilir. Bir bayrağı (boolean calisiyor) thread'ler arasında paylaşırken volatile anahtar kelimesi, her okumanın güncel
değeri görmesini sağlar. volatile görünürlüğü çözer ama "kontrol+artır" gibi bileşik
işlemleri atomik yapmaz — onlar için yine synchronized/atomik tipler gerekir.
Deadlock (kilitlenme)
İki thread, birbirinin tuttuğu kilidi beklerse sonsuza dek bekler — buna deadlock denir. Kaçınmanın klasik yolu: kilitleri her zaman aynı sırada almak.
ExecutorService: thread havuzları
Her iş için elle Thread yaratmak pahalıdır ve yönetimi zordur. Gerçek uygulamalar bunun
yerine bir thread havuzu kullanır: ExecutorService görevleri sınırlı sayıda thread
üzerinde çalıştırır, thread'leri yeniden kullanır.
Runnabledeğer döndürmez;Callable<T>bir değer döndürür (ve hata fırlatabilir).submit(callable)hemen birFuture<T>döndürür; sonucufuture.get()ile alırsın.invokeAll(görevler)tüm görevleri gönderip hepsinin bitmesini bekler.- İş bitince
shutdown()+awaitTermination(...)ile havuzu düzgün kapatırsın.
ExecutorService havuz = Executors.newFixedThreadPool(3); List<Future<Teklif>> sonuc = havuz.invokeAll(gorevler); havuz.shutdown();
CompletableFuture: modern async akış
CompletableFuture, asenkron işleri zincirleyerek ifade etmeni sağlar: bir sonucu üret
(supplyAsync), dönüştür (thenApply), iki sonucu birleştir (thenCombine). Bloklamadan,
okunabilir async akışlar kurarsın. Örnek 3 (./Ornek3.java) birden çok mağazadan paralel
fiyat sorgular (invokeAll + Future), en ucuzu bulur, sonra CompletableFuture ile iki
mağazanın kargo dahil fiyatını async olarak birleştirip karar verir.
Pratik notlar
- Mümkünse paylaşılan değişebilir durumdan kaçın; değişmez (immutable) veriyle çalışmak eşzamanlılık hatalarını kökten azaltır.
- Elle
ThreadyerineExecutorService/CompletableFuturegibi yüksek seviyeli araçları tercih et. - Paylaşılan koleksiyon gerekiyorsa
ConcurrentHashMap,CopyOnWriteArrayListgibi eşzamanlı (concurrent) koleksiyonları kullan. - Spring'de
@Asyncile bir metodu ayrı bir thread havuzunda çalıştırabilirsin; altında işte bu mekanizmalar vardır.
Özet
Paralelliğin gerçek kazancını (Örnek 1), yarış durumunu ve onu synchronized/ReentrantLock/
AtomicInteger ile çözmeyi (Örnek 2), ExecutorService/Callable/Future ve
CompletableFuture ile havuz tabanlı async işlemeyi (Örnek 3) gerçek senaryolarla gördük.
Görünürlük (volatile) ve deadlock kavramlarına değindik. Eşzamanlılık güçlüdür ama disiplin
ister: paylaşılan durumu koru, mümkünse ondan kaç. Sırada, çalışma zamanında kodu inceleyip
yönlendiren reflection ve annotations var.
▶ Kod Örnekleri(3)
Ornek1
çalıştırılabilir1// Ornek1: Gerçek senaryo — bir gösterge panelinin (dashboard) verilerini paralel toplamak.
2// Üç ayrı "servis" (satış, stok, müşteri) sırayla mı yoksa paralel mi daha hızlı?
3// Çalıştırma: java Ornek1.java
4import java.util.concurrent.ConcurrentHashMap;
5import java.util.Map;
6
7public class Ornek1 {
8
9 // Yavaş bir uzak servis çağrısını taklit eder (ağ gecikmesi gibi).
10 static String servisCagir(String ad, int gecikmeMs) {
11 uyu(gecikmeMs);
12 return ad + "-verisi";
13 }
14
15 public static void main(String[] args) throws InterruptedException {
16 // --- 1) Sıralı (sequential): her servis sırayla beklenir ---
17 long t0 = System.currentTimeMillis();
18 servisCagir("satis", 300);
19 servisCagir("stok", 300);
20 servisCagir("musteri", 300);
21 long sirali = System.currentTimeMillis() - t0;
22 System.out.println("Sıralı süre : ~" + sirali + " ms (yaklaşık toplam)");
23
24 // --- 2) Paralel: üç servisi aynı anda çağır ---
25 // Her thread sonucunu ortak, thread-safe bir haritaya yazar.
26 Map<String, String> sonuclar = new ConcurrentHashMap<>();
27
28 Runnable satisIsi = () -> sonuclar.put("satis", servisCagir("satis", 300));
29 Runnable stokIsi = () -> sonuclar.put("stok", servisCagir("stok", 300));
30 Runnable musteriIsi = () -> sonuclar.put("musteri", servisCagir("musteri", 300));
31
32 long t1 = System.currentTimeMillis();
33 Thread t_satis = new Thread(satisIsi, "satis-thread");
34 Thread t_stok = new Thread(stokIsi, "stok-thread");
35 Thread t_musteri = new Thread(musteriIsi, "musteri-thread");
36
37 t_satis.start();
38 t_stok.start();
39 t_musteri.start();
40
41 // join: üç thread de bitene kadar bekle. Olmazsa sonuçlar henüz hazır olmayabilir.
42 t_satis.join();
43 t_stok.join();
44 t_musteri.join();
45 long paralel = System.currentTimeMillis() - t1;
46
47 System.out.println("Paralel süre : ~" + paralel + " ms (en yavaş servis kadar)");
48 System.out.println("Toplanan veri : " + sonuclar);
49 System.out.printf("%nKazanç: paralel çalıştırma yaklaşık %.1fx daha hızlı.%n",
50 (double) sirali / Math.max(1, paralel));
51 }
52
53 static void uyu(long ms) {
54 try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
55 }
56}Ornek2
çalıştırılabilir1// Ornek2: Gerçek senaryo — konser bileti satışında yarış durumu (race condition).
2// 100 bilet var, 4 satış noktası aynı anda satıyor. Korumasız kod "fazla satış" yapar!
3// Çalıştırma: java Ornek2.java
4import java.util.concurrent.atomic.AtomicInteger;
5import java.util.concurrent.locks.ReentrantLock;
6
7public class Ornek2 {
8
9 static final int TOPLAM_BILET = 100;
10 static final int SATIS_NOKTASI = 4;
11 static final int DENEME = 50; // her nokta 50 kez satmayı dener (toplam 200 > 100)
12
13 public static void main(String[] args) throws InterruptedException {
14 System.out.println(TOPLAM_BILET + " bilet, " + SATIS_NOKTASI + " satış noktası, her biri "
15 + DENEME + " kez deniyor.\n");
16
17 // --- 1) Korumasız: race condition -> fazla satış ---
18 GuvensizGise guvensiz = new GuvensizGise();
19 calistir(() -> guvensiz.satisDene());
20 System.out.println("Korumasız gişe -> satılan: " + guvensiz.satilan
21 + (guvensiz.satilan > TOPLAM_BILET ? " ❗ FAZLA SATIŞ!" : ""));
22
23 // --- 2) synchronized ile koruma ---
24 SyncGise sync = new SyncGise();
25 calistir(() -> sync.satisDene());
26 System.out.println("synchronized gişe -> satılan: " + sync.satilan + " (doğru, en fazla " + TOPLAM_BILET + ")");
27
28 // --- 3) ReentrantLock ile koruma (daha esnek kilit) ---
29 LockGise lock = new LockGise();
30 calistir(() -> lock.satisDene());
31 System.out.println("Lock gişe -> satılan: " + lock.satilan + " (doğru)");
32
33 // --- 4) AtomicInteger ile kilitsiz koruma ---
34 AtomicGise atomik = new AtomicGise();
35 calistir(() -> atomik.satisDene());
36 System.out.println("Atomic gişe -> satılan: " + atomik.satilan() + " (doğru)");
37 }
38
39 // Verilen satış işini SATIS_NOKTASI kadar thread'de paralel çalıştırır.
40 static void calistir(Runnable satisDene) throws InterruptedException {
41 Thread[] threadler = new Thread[SATIS_NOKTASI];
42 for (int i = 0; i < SATIS_NOKTASI; i++) {
43 threadler[i] = new Thread(() -> {
44 for (int j = 0; j < DENEME; j++) satisDene.run();
45 });
46 threadler[i].start();
47 }
48 for (Thread t : threadler) t.join();
49 }
50
51 // Korumasız: kontrol et + artır iki ayrı adım; araya başka thread girebilir.
52 static class GuvensizGise {
53 int satilan = 0;
54 void satisDene() {
55 if (satilan < TOPLAM_BILET) { // <-- birden çok thread aynı anda "var" görebilir
56 Thread.yield(); // yarış penceresini genişletir (hatayı görünür kılar)
57 satilan++; // <-- hepsi satar -> fazla satış / tutarsız sayı
58 }
59 }
60 }
61
62 static class SyncGise {
63 int satilan = 0;
64 synchronized void satisDene() { // tüm "kontrol+artır" tek seferde, tek thread
65 if (satilan < TOPLAM_BILET) satilan++;
66 }
67 }
68
69 static class LockGise {
70 int satilan = 0;
71 private final ReentrantLock kilit = new ReentrantLock();
72 void satisDene() {
73 kilit.lock(); // kilidi al
74 try {
75 if (satilan < TOPLAM_BILET) satilan++;
76 } finally {
77 kilit.unlock(); // her durumda bırak
78 }
79 }
80 }
81
82 static class AtomicGise {
83 private final AtomicInteger satilan = new AtomicInteger(0);
84 void satisDene() {
85 // compareAndSet ile atomik "kontrol et ve artır" döngüsü.
86 int mevcut;
87 while ((mevcut = satilan.get()) < TOPLAM_BILET) {
88 if (satilan.compareAndSet(mevcut, mevcut + 1)) return;
89 }
90 }
91 int satilan() { return satilan.get(); }
92 }
93}Ornek3
çalıştırılabilir1// Ornek3: Gerçek senaryo — birden çok mağazadan paralel fiyat sorgulama.
2// ExecutorService + Callable + Future + invokeAll, ardından CompletableFuture ile async akış.
3// Çalıştırma: java Ornek3.java
4import java.util.ArrayList;
5import java.util.List;
6import java.util.concurrent.Callable;
7import java.util.concurrent.CompletableFuture;
8import java.util.concurrent.ExecutionException;
9import java.util.concurrent.ExecutorService;
10import java.util.concurrent.Executors;
11import java.util.concurrent.Future;
12import java.util.concurrent.TimeUnit;
13
14public class Ornek3 {
15
16 record Teklif(String magaza, double fiyat) {}
17
18 // Bir mağazanın fiyat servisini taklit eder (her biri farklı gecikme + fiyat).
19 static Teklif fiyatSorgula(String magaza, int gecikmeMs, double fiyat) {
20 try { Thread.sleep(gecikmeMs); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
21 return new Teklif(magaza, fiyat);
22 }
23
24 public static void main(String[] args) throws InterruptedException, ExecutionException {
25 // Thread havuzu: sınırlı sayıda thread, görevleri sırayla beslenir.
26 ExecutorService havuz = Executors.newFixedThreadPool(3);
27
28 // --- invokeAll: tüm görevleri gönder, hepsinin bitmesini bekle ---
29 List<Callable<Teklif>> gorevler = List.of(
30 () -> fiyatSorgula("Mağaza-A", 200, 1450),
31 () -> fiyatSorgula("Mağaza-B", 350, 1399),
32 () -> fiyatSorgula("Mağaza-C", 150, 1500),
33 () -> fiyatSorgula("Mağaza-D", 300, 1375)
34 );
35
36 long t0 = System.currentTimeMillis();
37 List<Future<Teklif>> futureler = havuz.invokeAll(gorevler);
38
39 List<Teklif> teklifler = new ArrayList<>();
40 for (Future<Teklif> f : futureler) {
41 teklifler.add(f.get()); // sonucu al (hazır değilse bekler)
42 }
43 long sure = System.currentTimeMillis() - t0;
44
45 System.out.println("Gelen teklifler (" + sure + " ms'de paralel toplandı):");
46 teklifler.forEach(t -> System.out.printf(" %-9s %.2f TL%n", t.magaza(), t.fiyat()));
47
48 Teklif enUcuz = teklifler.stream().min((a, b) -> Double.compare(a.fiyat(), b.fiyat())).orElseThrow();
49 System.out.printf("%nEn ucuz: %s -> %.2f TL%n", enUcuz.magaza(), enUcuz.fiyat());
50
51 // --- CompletableFuture: async akış (sorgula -> kargo ekle -> birleştir) ---
52 System.out.println("\n--- CompletableFuture ile async akış ---");
53 CompletableFuture<Double> a = CompletableFuture
54 .supplyAsync(() -> fiyatSorgula("Mağaza-A", 100, 1450).fiyat(), havuz)
55 .thenApply(fiyat -> fiyat + 50); // kargo ekle (dönüşüm)
56
57 CompletableFuture<Double> b = CompletableFuture
58 .supplyAsync(() -> fiyatSorgula("Mağaza-B", 120, 1399).fiyat(), havuz)
59 .thenApply(fiyat -> fiyat + 30);
60
61 // İki async sonucu birleştir (thenCombine): hangisi daha avantajlı?
62 CompletableFuture<String> karar = a.thenCombine(b, (fiyatA, fiyatB) ->
63 fiyatA <= fiyatB
64 ? "A daha avantajlı: " + fiyatA + " TL (kargo dahil)"
65 : "B daha avantajlı: " + fiyatB + " TL (kargo dahil)");
66
67 System.out.println(karar.get()); // akış tamamlanınca sonucu yazdır
68
69 // Havuzu düzgün kapat.
70 havuz.shutdown();
71 havuz.awaitTermination(2, TimeUnit.SECONDS);
72 System.out.println("\nThread havuzu kapatıldı.");
73 }
74}