Bölüm 9: İş Parçacıkları
Giriş
Öğrenme Çıktıları
İş parçacığı ile süreç, eşzamanlılık ile paralellik arasındaki farkı açıklar.
Runnable gerçekleyerek ya da Thread sınıfını genişleterek iş parçacığı yazar, start() ile başlatır.
Altı durumu ve sleep, join, wait, kilit beklemesi gibi geçişleri izler.
Yarış durumunu tanır; synchronized, volatile, Lock ve atomik sınıflarla önler.
wait/notify ve Condition ile üretici–tüketici yazar, kilitlenmeyi önler.
CountDownLatch, CyclicBarrier, Exchanger, Semaphore, ThreadLocal ve havuzları kullanır.
Temel Kavramlar
İş Parçacığı (Thread)
- İş parçacığı (thread), bir Java programındaki temel yürütme birimidir: komutları sırayla çalıştıran bağımsız bir akış.
- Her iş parçacığı, bir
Threadsınıfı nesnesi tarafından temsil edilir. - İş parçacıkları, çoklu görev (multithreading) ve eşzamanlı programlama (concurrent programming) için kullanılır.
- Her Java programı en az bir iş parçacığıyla başlar:
main()metodunu çalıştıran ana iş parçacığı (main thread).
Kovandaki her arı kendi işini yapar (bir iş parçacığı), ama hepsi aynı kovanı ve aynı bal peteklerini paylaşır (ortak bellek). İki arı aynı göze aynı anda bal koymaya çalışırsa sorun çıkar: bu bölümün konusu, bu “çarpışmaları” düzenlemektir.
Arayüz donmadan arka planda iş yapmak, çok çekirdekli işlemciyi kullanmak, ağ ya da dosya beklerken başka işe devam etmek.
Temel Kavramlar
Eşzamanlılık ve Paralellik
Birden fazla görevin aynı zaman aralığında ilerlemesi. Tek çekirdekte bile olur: işletim sistemi iş parçacıklarını çok kısa dilimlerle sırayla çalıştırır, biz aynı anda çalışıyor sanırız.
Görevlerin gerçekten aynı anda, farklı çekirdeklerde çalışması. Birden fazla çekirdek gerektirir; hesaplamayı hızlandırır.
Hangi iş parçacığının ne zaman, hangi çekirdekte çalışacağına JVM ve işletim sisteminin zamanlayıcısı (scheduler) karar verir; programcı bu sırayı bilemez.
Temel Kavramlar
Süreç ve İş Parçacığı
| Süreç (process) | İş parçacığı | |
|---|---|---|
| Bellek | Kendi adres alanı | Sürecin heap'ini paylaşır |
| Yığın | — | Her birinin kendi yığını |
| Oluşturma | Pahalı | Ucuz (hafif süreç) |
| İletişim | Dosya, soket, boru | Ortak nesneler |
| Risk | Birbirini bozamaz | Ortak veriyi bozabilir |
Temel Kavramlar
Java Sanal Makinesi (JVM)
JVM, iş parçacıklarını bağımsız yürütme birimleri olarak yöneten sanal bir ortamdır.
Her iş parçacığı oluşturulduğunda JVM ona ayrı bir yığın, PC yazmacı ve yerel metot yığını ayırır; sınıf alanı ve heap tek kopyadır.
Thread Sınıfı
Thread Sınıfı
- Her iş parçacığı,
java.langpaketindekiThreadsınıfının bir nesnesidir (import gerekmez). Thread, iş parçacığının yaşam döngüsünü yönetir; gerekli metotları ve özellikleri içerir.- Bir yol:
Thread'den türeyen alt sınıf yazmak. run()metodu geçersiz kılınarak (override) iş parçacığının ana mantığı tanımlanır.start()çağrılarak iş parçacığı başlatılır.
| Metot | Görevi |
|---|---|
start() | Yeni iş parçacığını başlatır, o da run()'ı çalıştırır |
run() | İş parçacığının yapacağı iş |
sleep(ms) | static: çalışanı belirli süre uyutur |
join() | O iş parçacığının bitmesini bekler |
yield() | static: yürütmeyi geçici olarak başkasına bırakır |
interrupt() | Kesme isteği gönderir |
currentThread() | static: şu an çalışan iş parçacığı |
getName(), getState(), isAlive() | Bilgi okuma |
Thread Sınıfı
İş Parçacığının Temel Nitelikleri (1)
run() metodunu içeren nesne. İş parçacığının ana mantığı burada tanımlanır.
İş parçacığının adı: main, Thread-0… Günlük kayıtları (log) ve teşhis için kullanılır. getName(), setName()
Tekil kimlik numarası; iş parçacığı oluşturulurken sistem tarafından otomatik atanır. threadId()
İş parçacığının ait olduğu grup; iş parçacıklarını düzenlemek ve yönetmek için kullanılır.
Diğer iş parçacıklarına hizmet eder, belirli görevleri periyodik yapar; JVM kapanırken bitmesi beklenmez.
Bağlam sınıf yükleyici: iş parçacığının sınıfları dinamik olarak yüklerken kullandığı sınıf yükleyici.
Bu nitelikler Thread nesnesinin alanlarıdır; okuma/yazma için get/set metotları vardır.
Thread Sınıfı
İş Parçacığının Temel Nitelikleri (2)
MIN_PRIORITY (1) ile MAX_PRIORITY (10) arasında tamsayı; varsayılan NORM_PRIORITY (5).
Mevcut durum: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED.
Kesilme isteği gelip gelmediğini gösteren bayrak; interrupt() ile işaretlenir.
Thread t = Thread.currentThread(); // çalışan System.out.println("Ad : " + t.getName()); // threadId(): Java 19+ (öncesinde getId()) System.out.println("Kimlik : " + t.threadId()); System.out.println("Öncelik : " + t.getPriority()); String grup = t.getThreadGroup().getName(); System.out.println("Grup : " + grup); System.out.println("Daemon : " + t.isDaemon()); System.out.println("Durum : " + t.getState());
Ad : main Kimlik : 1 Öncelik : 5 Grup : main Daemon : false Durum : RUNNABLE
Kod parçası main içindedir. Thread.currentThread() o satırı çalıştıran iş parçacığını verir; burada ana iş parçacığı.
Thread Sınıfı
JVM Başlangıcında İki İş Parçacığı Grubu
JVM başlarken oluşturulur; ana iş parçacığı ve bizim oluşturduklarımız varsayılan olarak buraya aittir.
JVM'in kendi iş parçacıkları: referans yönetimi, sonlandırıcı (Finalizer), sinyal gönderici (Signal Dispatcher)…
Tüm iş parçacıklarını listeleyelim
Set<Thread> hepsi =
Thread.getAllStackTraces().keySet();
for (Thread t : hepsi) {
System.out.println(t.getThreadGroup().getName()
+ " " + t.getName()
+ " daemon=" + t.isDaemon());
}system Notification Thread daemon=true main main daemon=false system Signal Dispatcher daemon=true system Reference Handler daemon=true system Attach Listener daemon=true InnocuousThreadGroup Common-Cleaner daemon=true system Finalizer daemon=true
Thread Sınıfı
JVM Başlangıcında Dört Temel İş Parçacığı
JVM başladığında ilk çalışan iş parçacığı. main() metodu burada çalışır; temel uygulama mantığı burada yürütülür.
Bellek yönetiminden sorumludur. Referansı kalmamış nesneleri bulup temizler; otomatik çalışır. HotSpot'ta JVM'in iç iş parçacıklarıdır, listede görünmez.
İşletim sisteminden gelen sinyalleri yönetir; örneğin Ctrl+C ile gelen kapatma isteğinde JVM'in güvenli kapanmasını başlatır.
Nesnelerin finalize() metodunu çağırır. finalize() Java 9'da kullanımdan kaldırıldı (deprecated); yerine try-finally veya AutoCloseable + try-with-resources kullanılır.
Yaşam Döngüsü
İş Parçacığı Durumları
İş parçacıkları yaşam döngüleri boyunca farklı durumlarda bulunur; getState() durumu Thread.State sabiti olarak verir.
| Durum | Anlamı | Nasıl girilir? |
|---|---|---|
NEW (yeni) | Nesne oluşturuldu, henüz başlatılmadı | new Thread(…); çıkış: start() |
RUNNABLE (çalışabilir) | Çalışıyor ya da işlemci sırası bekliyor | start() sonrası |
BLOCKED (engellenmiş) | synchronized blok/metoda girmek için monitör kilidini bekliyor | Kilit başkasındayken |
WAITING (bekliyor) | Süresiz bekliyor; başka bir iş parçacığı uyandırmalı | Object.wait(), Thread.join(), LockSupport.park() |
TIMED_WAITING | Süreli bekliyor | Thread.sleep(ms), wait(ms), join(ms), parkNanos, parkUntil |
TERMINATED (sonlandı) | Yürütmeyi tamamladı | run() bitince |
Dikkat: Java'da ayrı bir “RUNNING” durumu yoktur; çalışan ve sıra bekleyen iş parçacıklarının ikisi de RUNNABLE'dır.
Yaşam Döngüsü
Durum Diyagramı
NEW → RUNNABLE → TERMINATED. Biten iş parçacığı yeniden başlatılamaz.
Üç bekleme durumunun her birinden yeniden RUNNABLE'a dönülür.
BLOCKED: kilit için; WAITING: bir olay için; TIMED_WAITING: süre sınırlı.
Yaşam Döngüsü · Kodu adım adım çalıştır
Durumları Gözlemlemek: Adım Adım
uyuyan nesnesi oluşturuldu ama başlatılmadı: NEW.
Durum yazdırılır.
start(): yeni iş parçacığı çalışmaya hazır.
İkisi de sleep içinde: main 100 ms, uyuyan 1000 ms.
main uyandı; uyuyan hâlâ uyuyor: TIMED_WAITING.
bekleyen, uyuyan.join() çağırır: süresiz bekler, WAITING.
100 ms sonra durum yazdırılır.
main kilit'i tutuyor; engellenen aynı kilide girmek istiyor: BLOCKED.
Kilit hâlâ main'de; durum yazdırılır.
Blok bitince kilit bırakılır, engellenen girip biter. main bekleyen'i bekler; uyuyan 1 sn dolunca biter, bekleyen de biter.
run()'ı biten iş parçacığı: TERMINATED.
public static void main(String[] args) { Thread uyuyan = new Thread(() -> uyu(1000)); System.out.println("1) " + uyuyan.getState()); uyuyan.start(); uyu(100); System.out.println("2) " + uyuyan.getState()); Thread bekleyen = new Thread(() -> katil(uyuyan)); bekleyen.start(); uyu(100); System.out.println("3) " + bekleyen.getState()); Thread engellenen = new Thread(() -> { synchronized (kilit) { } }); synchronized (kilit) { engellenen.start(); uyu(100); System.out.println("4) " + engellenen.getState()); } katil(bekleyen); System.out.println("5) " + uyuyan.getState()); }
uyu(ms) ve katil(t): Thread.sleep ve t.join() çağrılarını try-catch ile saran yardımcı metotlar. Bekleme süreleri sayesinde çıktı hep böyledir.
Oluşturma ve Başlatma
Runnable Arayüzü
- Her iş parçacığı, çalıştırdığı bir
Runnablenesneye sahiptir. Runnablearayüzü yalnızcarun()metodunu içerir.- Bu arayüzü gerçekleyen (implements) sınıf,
run()metodunu tanımlar: iş parçacığının yapacağı iş. - Tek soyut metotlu (functional) bir arayüz olduğu için Java 8+ ile lambda ifadesi de yazılabilir.
JDK'daki tanım
package java.lang; public interface Runnable { void run(); }
Gerçekleyen sınıf
public class MyRunnable implements Runnable { @Override public void run() { // İş parçacığının yapması gereken görevler } }
Java 8+: lambda
Runnable r = () -> System.out.println("merhaba");Oluşturma ve Başlatma
İş Parçacığını Başlatma
public class Worker implements Runnable { // İş parçacığının görevleri burada tanımlanır public void run() { String ad = Thread.currentThread().getName(); System.out.println("Worker çalışıyor: " + ad); } public static void main(String[] args) { // İş parçacığını başlatma Worker w = new Worker(); Thread t = new Thread(w); t.start(); System.out.println("main devam ediyor"); } }
Threadyapıcısına parametre olarakRunnablenesne verilir.start()çağrılarak iş parçacığı başlatılır.run()doğrudan çağrılmaz: aksi takdirde kod, çağıranın iş parçacığında çalışır.
main devam ediyor Worker çalışıyor: Thread-0
Satırların sırası çalıştırmadan çalıştırmaya değişebilir: start() döner dönmez main devam eder; yeni iş parçacığının ne zaman çalışacağını zamanlayıcı belirler.
Oluşturma ve Başlatma · Bir olası sıra
start(): İki Yığın, İki Akış
main çalışıyor; Worker nesnesi heap'te oluşturulur. Henüz tek iş parçacığı var.
Thread nesnesi oluştu (NEW) ama henüz ayrı bir yürütme akışı ve yığın yok.
start(): JVM yeni bir yığın açar ve orada run()'ı başlatır. start() hemen döner.
Bir olası sıra: main önce davranır ve yazdırır.
main() bitti ama program bitmedi: JVM, daemon olmayan iş parçacıkları bitene kadar bekler.
Thread-0 kendi yığınında çalışır; ad onun yerel değişkenidir.
Thread-0 yazdırır.
run() bitti: Thread-0 TERMINATED. Çalışan iş parçacığı kalmadı, JVM kapanır.
public class Worker implements Runnable { // İş parçacığının görevleri burada tanımlanır public void run() { String ad = Thread.currentThread().getName(); System.out.println("Worker çalışıyor: " + ad); } public static void main(String[] args) { // İş parçacığını başlatma Worker w = new Worker(); Thread t = new Thread(w); t.start(); System.out.println("main devam ediyor"); } }
Oluşturma ve Başlatma
Thread Sınıfını Genişletme
public class Worker2 extends Thread { // İş parçacığının görevleri burada tanımlanır public void run() { String ad = getName(); // Thread'den kalıtıldı System.out.println("Worker çalışıyor: " + ad); } public static void main(String[] args) { // İş parçacığını oluşturma ve başlatma Worker2 w = new Worker2(); w.start(); System.out.println("main devam ediyor"); } }
- İş parçacığı,
Threadsınıfı genişletilerek (extends) de oluşturulabilir. Runnablegerçeklemek yerineThreadalt sınıfı yazılır;ThreadzatenRunnable'ı gerçekler.- Nesnenin kendisi bir
Threadolduğu için doğrudanw.start()vegetName()çağrılır.
main devam ediyor Worker çalışıyor: Thread-0
Sıra yine değişebilir.
Oluşturma ve Başlatma
start() mı, run() mı?
public class StartRunFarki { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { String ad = Thread.currentThread().getName(); System.out.println("run() içinde: " + ad); }); t.run(); // YANLIŞ: main çalıştırır t.start(); // DOĞRU: yeni iş parçacığı t.join(); } }
run() içinde: main run() içinde: Thread-0
| Çağrı | Ne olur? |
|---|---|
t.run() | Sıradan bir metot çağrısı: aynı iş parçacığında (main) çalışır, yeni yığın yok, bitene kadar main bekler. |
t.start() | JVM yeni bir iş parçacığı ve yığın oluşturur; run() orada çalışır, start() hemen döner. |
t.start() ikinci kez | IllegalThreadStateException: bir Thread nesnesi yalnızca bir kez başlatılabilir. |
run() içine yazarız, iş parçacığını başlatmak için start() çağırırız.Oluşturma ve Başlatma
Tek Miras Sorunu
- Java bir sınıfın yalnızca tek bir sınıftan kalıtım almasına izin verir.
- Çalıştırılabilir sınıf zaten başka bir sınıftan (
Ata) türemişse,Threadsınıfını genişletemez:extends Ata, Threadderlenmez. - Çözüm:
Runnablearayüzünü gerçekle ve sınıf içindeThreadtüründe bir alan tut. - Arayüz sayısında sınır yoktur; bu yüzden
Runnablegenellikle tercih edilir.
Oluşturma ve Başlatma
Tek Miras: Çözüm Kodu
class Ata { public void selamla() { System.out.println("Ata sınıfından merhaba"); } } class Worker extends Ata implements Runnable { private Thread thread; // Thread alan olarak public void run() { String ad = Thread.currentThread().getName(); System.out.println("run(): " + ad); } public void start() { thread = new Thread(this); thread.start(); } } public class TekMirasOrnegi { public static void main(String[] args) { Worker w = new Worker(); w.selamla(); // Ata'dan kalıtılan metot w.start(); // kendi start() metodumuz } }
Ata sınıfından merhaba run(): Thread-0
WorkerhemAta'nın metotlarını kalıtır hem de bir iş parçacığında çalışabilir.- Kendi
start()metodumuznew Thread(this)ile kendini birThread'e verir. this,Runnabletüründendir;Threadyapıcısı bunu kabul eder.
Oluşturma ve Başlatma
Üç Yol: Karşılaştırma
public class UcYol { static class Gorev implements Runnable { // 1) public void run() { System.out.println("1) Runnable sınıfı"); } } static class IsParcacigi extends Thread { // 2) public void run() { System.out.println("2) Thread alt sınıfı"); } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new Gorev()); Thread t2 = new IsParcacigi(); // 3) Lambda (Java 8+) Thread t3 = new Thread( () -> System.out.println("3) Lambda")); t1.start(); t1.join(); t2.start(); t2.join(); t3.start(); t3.join(); } }
1) Runnable sınıfı 2) Thread alt sınıfı 3) Lambda
| Yol | Ne zaman? |
|---|---|
implements Runnable | Sınıf başka sınıftan türemişse; iş ile iş parçacığı ayrı tutulur |
extends Thread | Kısa, bağımsız örnekler; kalıtım hakkı harcanır |
| Lambda (Java 8+) | Kısa görevler için en pratik yol |
Her start()'tan hemen sonra join() geldiği için çıktı sırası sabittir. static class: başka sınıfın içinde tanımlanan sınıf.
Oluşturma ve Başlatma · Bir olası sıra
İki İş Parçacığı: Bir Olası Sıra
main, A'yı başlatır. A artık RUNNABLE; ne zaman çalışacağı belli değil.
main, B'yi başlatır.
a.join(): main, A bitene kadar WAITING. Artık A ve B çalışır.
Zamanlayıcı A'yı çalıştırır: A kendi yığınında gorev()'i yürütür.
Bağlam değişimi (context switch): şimdi B çalışıyor. B'nin i'si ayrı bir yerel değişkendir.
Tekrar A.
Tekrar B. Ardından ikisi de biter (TERMINATED).
a.join() döner, b.join() B zaten bittiği için hemen döner; main yazdırır.
public class IkiIsParcacigi { static void gorev() { String ad = Thread.currentThread().getName(); for (int i = 1; i <= 2; i++) { System.out.println(ad + ": adım " + i); } } public static void main(String[] args) throws InterruptedException { Thread a = new Thread(() -> gorev(), "A"); Thread b = new Thread(() -> gorev(), "B"); a.start(); b.start(); a.join(); b.join(); System.out.println("main: bitti"); } }
Olası sıralardan biri. Gerçek bir çalıştırma: B1 · B2 · A1 · A2. Kesin olan: “main: bitti” en sondadır.
Yönetim ve Durdurma
İş Parçacığı Yönetimi: sleep, join, yield
Çalışan iş parçacığını belirli bir süre uyutur (TIMED_WAITING). Kilit tutuyorsa bırakmaz.
Çağıran iş parçacığı, hedef iş parçacığı bitene kadar bekler. join(ms): en çok ms kadar.
Yürütmeyi geçici olarak başka iş parçacığına bırakma ipucu; zamanlayıcı yok sayabilir.
public class JoinOrnegi { static long sonuc = 0; public static void main(String[] args) throws InterruptedException { Thread hesap = new Thread(() -> { long toplam = 0; for (int i = 1; i <= 1_000_000; i++) { toplam += i; } sonuc = toplam; }); hesap.start(); System.out.println("join öncesi : " + sonuc); hesap.join(); // hesap bitene kadar bekle System.out.println("join sonrası: " + sonuc); } }
join öncesi : 0 join sonrası: 500000500000
join() olmadan main, hesap bitmeden sonuc'u okur: 0 görür. join() sonrası hesap kesinlikle bitmiştir ve yazdığı değer görünür.
sleep ve join, InterruptedException fırlatabilir: yakalanmalı ya da throws ile bildirilmelidir.
Yönetim ve Durdurma
Arka Plan (Daemon) İş Parçacıkları
public class DaemonOrnegi { public static void main(String[] args) throws InterruptedException { Thread saat = new Thread(() -> { int s = 0; while (true) { System.out.println("tik " + s++); try { Thread.sleep(300); } catch (InterruptedException e) { return; } } }); saat.setDaemon(true); // start()'tan ÖNCE saat.start(); Thread.sleep(1000); System.out.println("main bitti, JVM kapanıyor"); } }
tik 0 tik 1 tik 2 tik 3 main bitti, JVM kapanıyor
- JVM, daemon olmayan tüm iş parçacıkları bitince kapanır; daemon iş parçacıklarının bitmesi beklenmez.
saatsonsuz döngüde olsa da main bitince program sonlanır.setDaemon(true),start()'tan önce çağrılmalı; sonra çağrılırsaIllegalThreadStateException.
Yönetim ve Durdurma
İş Parçacığını Durdurma
- İş parçacığı,
run()metodu tamamlandığında durur. - JVM kapanır:
System.exit()(içerideRuntime.exit()) çağrıldığında ya da daemon olmayan tüm iş parçacıkları bittiğinde. - Bir iş parçacığını dışarıdan durdurmanın doğru yolu iş birliğidir: iş parçacığı periyodik olarak bir bayrak (flag) kontrol eder.
- Bayrak, değişikliği diğer iş parçacıklarının görebilmesi için
volatiletanımlanır. - Alternatif:
interrupt()ile kesme isteği (ileride).
Kullanımdan kaldırılmış (deprecated) stop(), iş parçacığını işinin ortasında öldürür; tuttuğu kilitler bırakılır ve nesneler yarım güncellenmiş kalabilir. Java 20+'da artık çalışmaz:
Thread t = new Thread(() -> { }); try { t.stop(); } catch (UnsupportedOperationException e) { System.out.println("stop(): desteklenmiyor"); }
stop(): desteklenmiyor
Yönetim ve Durdurma
Bayrak ile Durdurma
class Worker extends Thread { private volatile boolean flag = false; public void finish() { flag = true; } public void run() { while (!flag) { System.out.println("çalışıyor..."); try { Thread.sleep(100); } catch (InterruptedException e) { return; } } System.out.println("bayrak görüldü, bitiyor"); } } public class BayrakDurdurma { public static void main(String[] args) throws InterruptedException { Worker w = new Worker(); w.start(); Thread.sleep(350); w.finish(); // bayrağı kaldır w.join(); // bitmesini bekle System.out.println("main: işçi durdu"); } }
çalışıyor... çalışıyor... çalışıyor... çalışıyor... bayrak görüldü, bitiyor main: işçi durdu
finish()yalnızca bayrağı kaldırır; iş parçacığı döngünün başında bunu görür verun()'dan kendisi çıkar.- main 350 ms uyurken işçi 0, 100, 200, 300 ms'de dört kez yazdı.
volatileolmasaydı işçi bayrağın değiştiğini hiç görmeyebilirdi (bkz. Görünürlük).
Yönetim ve Durdurma
Referans ile Durdurma
class Worker implements Runnable { private volatile Thread thread; public void start() { thread = new Thread(this); thread.start(); } public void finish() { thread = null; } public void run() { Thread ben = Thread.currentThread(); while (thread == ben) { // ... iş ... } System.out.println("run() bitti"); } } public class ReferansDurdurma { public static void main(String[] args) throws Exception { Worker w = new Worker(); w.start(); Thread.sleep(200); w.finish(); Thread.sleep(200); System.out.println("main: bitti"); } }
run() bitti main: bitti
- Sınıf
Runnablegerçekler; çalışanThreadnesnesivolatilebir alanda tutulur. - Döngü, alan hâlâ kendi iş parçacığını gösterdikçe sürer:
Thread.currentThread()ile karşılaştırılır. finish()alanınullyapar; koşul bozulur,run()biter.currentThread()static birThreadmetodudur;Thread'i genişletmeyen sınıftaThread.ön eki şarttır.
Senkronizasyon
İşlemleri Doğru Sırayla Gerçekleştirmek
Her iş parçacığının çalışma hızı ve sıklığı değişkendir. İşletim sistemleri öncelikleri görmezden gelebilir; sıraya öncelikle güvenilmez.
Bir iş parçacığı değişkeni okuduktan sonra başkası değeri güncelleyebilir; ilki güncel olmayan değerle çalışmaya devam eder.
Derleyici, JIT ve işlemci, optimizasyon için komutların sırasını değiştirebilir; tek iş parçacığında fark edilmez, birden fazlasında beklenmeyen davranış doğurur.
Senkronizasyon
Yarış Durumu (Race Condition)
public class YarisDurumu { static int sayac = 0; // paylaşılan veri public static void main(String[] args) throws InterruptedException { Runnable arttir = () -> { for (int i = 0; i < 100_000; i++) { sayac++; // oku, 1 ekle, yaz: atomik DEĞİL } }; Thread t1 = new Thread(arttir); Thread t2 = new Thread(arttir); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("Beklenen: 200000"); System.out.println("Bulunan : " + sayac); } }
Beklenen: 200000 Bulunan : 138001
Beklenen: 200000 Bulunan : 135352
Beklenen: 200000 Bulunan : 136776
Her çalıştırmada farklı ve 200000'den küçük bir sonuç: bazı artışlar kayboluyor.
Senkronizasyon · Bir olası sıra
sayac++ Neden Kaybolur?
Başta sayac = 0. İki iş parçacığı da sayac++ yapacak; doğru sonuç 2 olmalı.
t1 okur: yazmacına 0 gelir.
Bağlam değişimi! t2 de okur: o da 0 görür.
t1 kendi yazmacında 1 ekler.
t1 yazar: sayac = 1.
t2 kendi (eski) değerine 1 ekler: 1.
t2 yazar: sayac = 1. İki artış yapıldı ama sonuç 1: t1'in artışı kayboldu.
// sayac++ işlemcide üç adımdır: int yazmac = sayac; // 1) oku yazmac = yazmac + 1; // 2) 1 ekle sayac = yazmac; // 3) yaz
Bu, olası sıralardan yalnızca biridir; araya girme olmazsa sonuç doğru çıkar. Hata bu yüzden rastgele görünür ve test etmesi zordur. Çağrı yığını kutusu o anda çalışan iş parçacığını gösterir.
Senkronizasyon
Senkronizasyon Seçenekleri
Bir iş parçacığının bitmesini beklemek.
Nesnenin monitör kilidiyle karşılıklı dışlama.
ReentrantLock, ReadWriteLock: açık kilitleme.
CyclicBarrier, Semaphore, Exchanger, CountDownLatch.
Okuma/yazmanın tüm iş parçacıklarınca görünmesi.
AtomicInteger gibi bölünemez işlemler.
sleep), yol verme (yield) ve zamanlayıcılar (timer). Bunlar sırayı garanti etmez; “biraz bekleyeyim, öbürü bitirmiş olur” diye kod yazılmaz.Senkronizasyon · synchronized
synchronized ve Monitör
- Her nesnenin, kilitlenip açılabilen bir monitörü (monitor, iç kilit) vardır.
- Monitöre aynı anda yalnızca bir iş parçacığı sahip olabilir.
- t1 monitöre sahipken t2 de isterse t2 bekler (
BLOCKED). - Monitör bırakılınca bekleyenler rekabet eder, yalnızca biri sahip olur.
- Monitör
synchronizedile alınır; blok bitince (istisna ile çıkılsa bile) otomatik bırakılır. - Böylece karşılıklı dışlama (mutual exclusion) sağlanır, veri bütünlüğü korunur.
Sözdizimi
synchronized (o) { // o'nun monitörü bu blokta bizde }
Runnable r = () -> {
for (int i = 0; i < 50_000; i++) {
synchronized (o) { // o'nun monitörünü al
ortak++;
} // monitör bırakılır
}
};100000
Senkronizasyon
Monitör Adım Adım
// t1 ve t2 aynı kodu çalıştırır synchronized (o) { ortak++; // kritik bölge }
Kritik bölge (critical section): paylaşılan veriye erişen ve aynı anda yalnızca bir iş parçacığının çalıştırması gereken kod parçası.
t1 ve t2 aynı anda synchronized (o) satırına gelir; monitör boştur.
t1 kazanır ve monitörü alır. t2 giriş kümesinde BLOCKED bekler.
t1 bloktan çıkar: monitör otomatik bırakılır. Bekleyenler rekabet eder.
t2 monitörü alır ve kritik bölgeye girer.
t2 de çıkar. ortak++ iki kez, birbirine karışmadan yapıldı.
Senkronizasyon
Metotları Senkronize Tanımlama
synchronized metot
class C { synchronized void p() { ... } static synchronized void q() { ... } //... }
Eşdeğeri: synchronized blok
class C { void p() { synchronized (this) { ... } } static void q() { synchronized (C.class) { ... } } //... }
- Metodu
synchronizedtanımlamak, gövdesini birsynchronizedbloğa almakla aynıdır. - Örnek metot →
thisnesnesinin monitörü. staticmetot → sınıfınC.classnesnesinin monitörü (nesne yok).- Bu yüzden bir
static synchronizedile bir örneksynchronizedmetot birbirini engellemez: farklı kilitler. - Blok biçimi, yalnızca gerçekten gereken satırları kilitlemeye izin verir.
... yerine gövde yazıldığında (örneğin n++, s++) iki biçim derlenir ve aynı davranır.
Senkronizasyon
Yarışı Önlemek
class Sayac { private int deger = 0; public synchronized void arttir() { deger++; } public synchronized int getDeger() { return deger; } } public class SenkronSayac { public static void main(String[] args) throws InterruptedException { Sayac sayac = new Sayac(); Runnable gorev = () -> { for (int i = 0; i < 100_000; i++) { sayac.arttir(); } }; Thread t1 = new Thread(gorev); Thread t2 = new Thread(gorev); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("Sonuç: " + sayac.getDeger()); } }
Sonuç: 200000
arttir()artıksynchronized: oku–ekle–yaz adımları bölünmeden yapılır.- Okuyan
getDeger()da senkronize olmalı; yoksa güncel değeri görmesi garanti değildir. - Bedeli: iş parçacıkları sırayla girer, biraz yavaşlar. Yalnızca paylaşılan veriyi koruyun.
Senkronizasyon
Karşılıklı Dışlama: OncelikliKuyruk
class OncelikliKuyruk { private int[] veri; private int boyut; public OncelikliKuyruk(int n) { veri = new int[n]; } public synchronized void ekle(int o) { if (boyut == veri.length) throw new HeapFullException(); veri[boyut++] = o; } public synchronized int cikar() { // en küçüğü int enk = 0; for (int i = 1; i < boyut; i++) { if (veri[i] < veri[enk]) enk = i; } int sonuc = veri[enk]; veri[enk] = veri[--boyut]; return sonuc; } public synchronized String toString() { return Arrays.toString(Arrays.copyOf(veri, boyut)); } }
Tüm metotlar aynı monitörü (this) kullanır.
OncelikliKuyruk k =
new OncelikliKuyruk(3);
k.ekle(5);
k.ekle(2);
k.ekle(8);
System.out.println(k);
int x = k.cikar();
System.out.println(x);
System.out.println(k);
k.ekle(1);
try {
k.ekle(9);
} catch (HeapFullException e) {
String m = e.getMessage();
System.out.println(m);
}[5, 2, 8] 2 [5, 8] Kuyruk dolu
Görünürlük · volatile
volatile Nitelikler
- Performans için değerler önbellekte ya da yazmaçta (register) tutulabilir; o zaman diğer iş parçacıkları güncel değere erişemez.
volatile, değişkenin her okunuşta ana bellekten okunmasını, her yazılışta hemen görünür olmasını garanti eder.longvedoubledışındaki değişkenlerin tek okuma/yazması zaten atomiktir;volatile long/doubleda atomik olur.- Paylaşılan basit bir bayrak için
synchronizedve kilit gerektirmez. - Ama yalnızca okuma ve yazma atomiktir:
x++gibi oku–değiştir–yaz işlemi atomik değildir.
Görünürlük
Görünürlük Sorunu
public class GorunurlukSorunu { static boolean dur = false; // volatile DEĞİL! public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { long tur = 0; while (!dur) { tur++; } System.out.println("iş parçacığı durdu"); }); t.setDaemon(true); t.start(); Thread.sleep(500); dur = true; System.out.println("main: dur = true"); t.join(2000); // en çok 2 sn bekle boolean canli = t.isAlive(); System.out.println("hâlâ çalışıyor mu? " + canli); } }
main: dur = true hâlâ çalışıyor mu? true
main: dur = true iş parçacığı durdu hâlâ çalışıyor mu? false
volatile olmadan JIT derleyicisi dur'u bir kez okuyup döngüyü while (true) gibi çalıştırabilir: iş parçacığı değişikliği hiç görmez. Sonuç JVM'e göre değişebilir; doğruluk şansa bırakılmaz.
setDaemon(true): takılan iş parçacığı programın kapanmasını engellemesin diye.
Görünürlük · volatile
volatile Atomik Değildir, final ile Birleşmez
static volatile int sayac = 0; // main içinde: Runnable r = () -> { for (int i = 0; i < 100_000; i++) sayac++; };
volatile sayac: 163299 volatile sayac: 164441 volatile sayac: 199332
Görünürlük tamam, ama iki iş parçacığı yine aynı değeri okuyup aynı sonucu yazabiliyor. Çözüm: synchronized ya da AtomicInteger.
public class FinalVolatile { private final volatile int x = 5; }
FinalVolatile.java:2: error: illegal combination of modifiers: final and volatile
private final volatile int x = 5;
^
1 error| final | volatile |
|---|---|
| Bir kez atanır (yapıcıda); sonra değişmez | Değeri değişebilir |
| Yapıcı bitince herkes aynı değeri görür | Her okuma güncel değeri döndürür |
İkisi senkronizasyon ihtiyacını farklı yoldan giderir; birlikte kullanılamaz.
Açık Kilitler
Açık Kilitleme (Explicit Locking)
public class Ornek { private final Lock kilit = new ReentrantLock(); private int sayac = 0; public void calistir() { kilit.lock(); try { // kritik bölge sayac++; } finally { kilit.unlock(); } } public static void main(String[] args) throws InterruptedException { Ornek o = new Ornek(); Runnable r = () -> { for (int i = 0; i < 100_000; i++) o.calistir(); }; Thread a = new Thread(r), b = new Thread(r); a.start(); b.start(); a.join(); b.join(); System.out.println("Sonuç: " + o.sayac); } }
- Karmaşık senkronizasyon senaryoları için kullanılır.
java.util.concurrent.lockspaketindekiLockarayüzü;ReentrantLockveReadWriteLockgelişmiş, özelleştirilebilir kilitler sunar.- Esneklik ve performans iyileştirmeleri sağlar: denemeli, süreli, kesilebilir kilit alma.
- Kullanırken dikkatli olunmalı: kilidi siz açmalısınız.
- İçe aktarma:
java.util.concurrent.locks.*
Sonuç: 200000
Açık Kilitler
Kilidi finally Bloğunda Aç
Doğru kalıp
public void calistir() { Lock kilit = ...; kilit.lock(); try { // korunan kaynağa erişim } finally { kilit.unlock(); } }
lock(),try'dan hemen önce çağrılır.unlock()finallyiçindedir: kritik bölgede istisna olsa bile kilit her durumda bırakılır.
Yanlış: finally yok
kilit.lock(); islemYap(); // istisna fırlatırsa... kilit.unlock(); // ...bu satıra hiç gelinmez
Kilit kilitli kalır; onu isteyen her iş parçacığı sonsuza dek bekler. synchronized'da bu sorun yoktur: blok bitince kilit kendiliğinden bırakılır.
Yanlış: alınmamış kilidi açmak
Lock kilit = new ReentrantLock(); kilit.unlock(); // hiç alınmamış kilit
Exception in thread "main" java.lang.IllegalMonitorStateException
Açık Kilitler · Lock
tryLock(): Beklemeden ya da Süreli Kilit Alma
static void calistir() throws InterruptedException { if (kilit.tryLock()) { // alındı try { System.out.println("1: alındı"); } finally { kilit.unlock(); } } else { // alınamadı, başka bir şey yap System.out.println("1: alınamadı"); } if (kilit.tryLock(3, TimeUnit.SECONDS)) { try { System.out.println("2: alındı"); } finally { kilit.unlock(); } } else { // 3 saniye içinde alınamadı System.out.println("2: alınamadı"); } }
tryLock(): kilit boşsa alır,true; doluysa beklemedenfalse.tryLock(3, SECONDS): en çok 3 sn bekler;InterruptedExceptionbildirilmelidir.
kilit.lock(); // kilidi main tutuyor Thread t = new Thread(() -> { try { calistir(); } catch (InterruptedException e) { } }); t.start(); Thread.sleep(1000); System.out.println("main: kilidi bırakıyor"); kilit.unlock(); t.join();
1: alınamadı main: kilidi bırakıyor 2: alındı
Açık Kilitler · Lock
ReentrantLock Sınıfı
- Adı “yeniden girişe izin veren” (reentrant) anlamına gelir.
- Kilidi tutan iş parçacığı aynı kilidi tekrar alabilir; beklemez, yalnızca bir sayaç artar. Kilit, sayaç 0'a inince bırakılır.
- Birden fazla
ReentrantLockkullanılarak iç içe kritik bölgelere erişilebilir. - Adil kilitleme (fair lock):
new ReentrantLock(true)ile kilit, en uzun bekleyene verilir. synchronizedda yeniden girişlidir:synchronizedbir metot aynı nesnenin başka birsynchronizedmetodunu çağırabilir.
ReentrantLock kilit1 = new ReentrantLock(); // true: adil (fair) kilit ReentrantLock kilit2 = new ReentrantLock(true); // ... kilit1.lock(); try { // kritik bölgeye erişim kilit2.lock(); try { // iç içe kritik bölgeye erişim System.out.println("iki kilit de alındı"); } finally { kilit2.unlock(); } } finally { kilit1.unlock(); } System.out.println("adil mi? " + kilit2.isFair());
iki kilit de alındı adil mi? true
Açık Kilitler · Kodu adım adım çalıştır
Yeniden Giriş: Adım Adım
main, dis()'i çağırır. Kilit boş.
dis() kilidi alır: sahibi main, tutma sayısı 1.
yaz sayıyı yazdırır.
ic() aynı kilidi ister. Sahibi zaten main olduğu için beklemez: sayı 2 olur.
Yazdırılır.
ic()'nin finally'si: unlock() sayıyı 1'e indirir; kilit hâlâ main'de.
dis()'in finally'si: sayı 0, kilit gerçekten bırakıldı.
Her lock() için bir unlock() gerekir.
import java.util.concurrent.locks.ReentrantLock; public class YenidenGiris { static final ReentrantLock kilit = new ReentrantLock(); static void yaz(String yer) { System.out.println(yer + kilit.getHoldCount()); } static void dis() { kilit.lock(); try { yaz("dis(): "); ic(); } finally { kilit.unlock(); } } static void ic() { kilit.lock(); // beklemeden girer try { yaz("ic(): "); } finally { kilit.unlock(); } } public static void main(String[] args) { dis(); yaz("main: "); } }
Açık Kilitler · Lock
ReadWriteLock Sınıfı
Birden fazla iş parçacığı veriyi aynı anda okuyabilir; okurken birbirini engellemez (paralel okuma).
Bir iş parçacığı yazarken diğerlerinin okuması da yazması da engellenir; yazma güvenli ve atomik olur.
Verinin sık okunup nadiren yazıldığı durumlarda (önbellek, ayar tablosu) performansı artırır.
Açık Kilitler · Lock
ReadWriteLock Kodu
private final Map<String, String> tablo = new HashMap<>(); private final ReadWriteLock kilit = new ReentrantReadWriteLock(); public String oku(String anahtar) { kilit.readLock().lock(); // okuma kilidi al try { return tablo.get(anahtar); // veriyi oku } finally { kilit.readLock().unlock(); } } public void yaz(String anahtar, String deger) { kilit.writeLock().lock(); // yazma kilidi al try { tablo.put(anahtar, deger); // veriyi yaz } finally { kilit.writeLock().unlock(); } }
AyarTablosu a = new AyarTablosu(); a.yaz("dil", "tr"); Runnable okuyucu = () -> System.out.println( Thread.currentThread().getName() + ": " + a.oku("dil")); Thread o1 = new Thread(okuyucu, "Okuyucu-1"); Thread o2 = new Thread(okuyucu, "Okuyucu-2"); o1.start(); o2.start(); o1.join(); o2.join();
Okuyucu-2: tr Okuyucu-1: tr
İki okuyucu okuma kilidini aynı anda tutabilir; sıraları değişebilir. ReentrantReadWriteLock, ReadWriteLock arayüzünün gerçeklemesidir.
Açık Kilitler
Açık (Explicit) ve Dolaylı (Implicit) Kilitleme
Açık: Lock | Dolaylı: synchronized | |
|---|---|---|
| Kontrol | Geliştirici lock()/unlock() çağırır | Otomatik: blok girişinde alınır, çıkışta bırakılır |
| Sınıflar | Lock arayüzü, ReentrantLock, ReentrantReadWriteLock | Anahtar kelime; her nesnenin monitörü |
| Beklemeden deneme | tryLock(), tryLock(süre): engellemeden koşullu alma | Yok: kilit boşalana kadar bekler |
| Kesilebilir bekleme | lockInterruptibly() | Yok |
| Adillik, koşullar | new ReentrantLock(true), birden çok Condition | Yok; tek bekleme kümesi |
| Yeniden giriş | ReentrantLock yeniden girişlidir | Her zaman yeniden girişlidir |
| Risk | unlock() unutulursa kilit sonsuza dek kalır | Unutma riski yok |
| Ne zaman? | Gelişmiş kontrol gerektiğinde | Basit senaryolar, daha okunur kod |
Açık Kilitler · Kilitlenme
Kilitlenme (Deadlock)
static void calis(Object x, Object y, String ad) { synchronized (x) { // 1. kilit uyu(100); synchronized (y) { // 2. kilit System.out.println(ad + " bitti"); } } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> calis(a, b, "T1")); Thread t2 = new Thread(() -> calis(b, a, "T2")); t1.setDaemon(true); t2.setDaemon(true); t1.start(); t2.start(); Thread.sleep(500); String d = t1.getState() + " " + t2.getState(); System.out.println(d); long[] k = ManagementFactory.getThreadMXBean() .findDeadlockedThreads(); int n = (k == null) ? 0 : k.length; System.out.println("Kilitlenmiş: " + n); }
BLOCKED BLOCKED Kilitlenmiş: 2
İki ya da daha fazla iş parçacığının, birbirinin tuttuğu kilidi sonsuza dek beklemesi. Kilitler farklı sırada alındığında oluşur. uyu(ms): sleep'i try-catch ile sarar.
Açık Kilitler · Kilitlenme
Kilitlenmeyi Önlemek
Tek değişiklik: aynı sıra
Thread t1 = new Thread(() -> calis(a, b, "T1")); Thread t2 = new Thread(() -> calis(a, b, "T2"));
T1 bitti T2 bitti TERMINATED TERMINATED Kilitlenmiş: 0
Herkes önce a'yı, sonra b'yi alır: döngü oluşamaz. T2, a'yı bekler; T1 bitince alır ve o da biter.
Tüm iş parçacıkları kilitleri aynı sırayla alsın (ör. kimlik numarasına göre).
tryLock(süre) alamazsa elindekileri bırakıp sonra tekrar dener.
Kilit tutarken başka kilit istemekten ve uzun işlerden kaçının.
jstack ya da ThreadMXBean.findDeadlockedThreads() kilitlenmeyi gösterir.
Koşul Senkronizasyonu
Durum (Condition) Senkronizasyonu
- Ortak bir kaynağa yalnızca belirli bir koşul sağlandığında güvenli erişmeyi sağlar.
- Örnek: tüketici, kuyruk boş değilken öğe almalı; üretici, kuyruk dolu değilken eklemeli.
- Kontrollü güncelleme için nesne kilidi gerekir; ayrıca koşul sağlanana kadar beklemek gerekir.
- Ama kilidi tutarken beklenemez: koşulu değiştirecek iş parçacığı kilidi alamaz, herkes takılır.
1) Object.wait() ve Object.notify() / notifyAll()
2) Condition arayüzü (Lock ile)
3) Senkronizasyon nesneleri (CountDownLatch, Semaphore…) ve hazır BlockingQueue
Yanlış: meşgul bekleme
while (kuyruk.bosMu()) { // boşuna işlemci yakar; kilit tutuluyorsa // üretici hiç ekleyemez }
Koşul Senkronizasyonu
Object.wait() ve Object.notify()
- Her nesnenin bir bekleme kümesi (wait set) vardır.
- Kilidi tutan iş parçacığı
wait()çağırır: kümeye girer, kilidi bırakır, bloke olur. notify()birini,notifyAll()hepsini uyandırır;wait(ms)süre dolunca da döner.- Uyanan, kilidi yeniden alınca kaldığı yerden devam eder.
- Kural 1: yalnızca
synchronizediçinde çağrılır; yoksaIllegalMonitorStateException: current thread is not owner. - Kural 2: koşul
ifile değilwhileile denetlenir: uyanınca koşul yine bozulmuş olabilir (ya da sahte uyanma).
Koşul Senkronizasyonu
BlockingQueue: wait() ve notifyAll() ile
class BlockingQueue { private Object[] veri; private int bas = 0; private int son = 0; private int buyukluk = 0; public BlockingQueue(int boyut) { veri = new Object[boyut]; } public synchronized int getSize() { return buyukluk; } public synchronized void add(Object oge) throws InterruptedException { while (buyukluk == veri.length) { wait(); } veri[son] = oge; son = (son + 1) % veri.length; buyukluk++; notifyAll(); }
public synchronized Object remove() throws InterruptedException { while (buyukluk == 0) { wait(); } Object oge = veri[bas]; bas = (bas + 1) % veri.length; buyukluk--; notifyAll(); return oge; } }
- Dairesel (circular) dizi:
basçıkarılacak,soneklenecek konum;% veri.lengthbaşa sarar. - Doluysa
add, boşsaremovebekler; her değişiklikten sonranotifyAll()bekleyenleri uyandırır.
Koşul Senkronizasyonu · Bir olası sıra
Üretici–Tüketici: Adım Adım
Üretici add(1): yer var, 1 eklenir; son 1'e ilerler.
add(2): son 2 % 2 = 0'a sarar. Kuyruk dolu.
add(3): dolu olduğu için wait(): üretici bekleme kümesine girer ve kilidi bırakır.
Tüketici kilidi alabildi: remove() 1'i alır, bas 1 olur.
notifyAll(): üretici uyanır ama kilit hâlâ tüketicide; kilidi bekler. remove 1 döndürür.
Üretici kilidi alır, while koşulunu yeniden denetler: dolu değil. 3, veri[0]'a yazılır.
Tüketici 2'yi alır; bas başa sarar.
Tüketici 3'ü alır. Kuyruk boş.
Bir sonraki remove(): boş olduğu için tüketici bekler; üretici 4'ü ekleyip notifyAll() yapınca uyanacak.
public synchronized void add(Object oge) throws InterruptedException { while (buyukluk == veri.length) { wait(); } veri[son] = oge; son = (son + 1) % veri.length; buyukluk++; notifyAll(); } public synchronized Object remove() throws InterruptedException { while (buyukluk == 0) { wait(); } Object oge = veri[bas]; bas = (bas + 1) % veri.length; buyukluk--; notifyAll(); return oge; }
Kapasite 2. Bu, olası sıralardan biridir; gerçek sıra zamanlayıcıya bağlıdır ama kuyruk hiçbir sırada taşmaz ya da boşken okunmaz.
Koşul Senkronizasyonu
Üretici–Tüketici: Gerçek Çalıştırma
BlockingQueue kuyruk = new BlockingQueue(2); Thread uretici = new Thread(() -> { try { for (int i = 1; i <= 4; i++) { kuyruk.add(i); System.out.println("üretildi: " + i); } } catch (InterruptedException e) { } }); Thread tuketici = new Thread(() -> { try { for (int i = 1; i <= 4; i++) { Thread.sleep(100); Object x = kuyruk.remove(); System.out.println("tüketildi: " + x); } } catch (InterruptedException e) { } }); uretici.start(); tuketici.start(); uretici.join(); tuketici.join(); System.out.println("kalan: " + kuyruk.getSize());
üretildi: 1 üretildi: 2 üretildi: 3 tüketildi: 1 üretildi: 4 tüketildi: 2 tüketildi: 3 tüketildi: 4 kalan: 0
Satırların sırası değişebilir. Burada “üretildi: 3”, “tüketildi: 1”den önce göründü: tüketici remove'dan döndükten sonra, yazdıramadan üretici çalıştı.
Koşul Senkronizasyonu · Condition
Condition Arayüzü
- Bir iş parçacığının durum değişene kadar beklemesini, durum değişince kaldığı yerden devam etmesini sağlar.
wait()/notify()işlevselliğiniLockdünyasına taşır;ReentrantLockile birlikte kullanılır:kilit.newCondition().await(): kilidi bırakır ve iş parçacığını askıya alır.signal()/signalAll(): bekleyen bir / tüm iş parçacıklarını uyandırır.- Bir kilide birden çok koşul bağlanabilir: üreticiler ve tüketiciler ayrı kuyrukta bekler.
class BlockingQueue { private final Lock kilit = new ReentrantLock(); private final Condition doluDegil = kilit.newCondition(); private final Condition bosDegil = kilit.newCondition(); private Object[] veri; private int bas = 0; private int son = 0; private int buyukluk = 0; public BlockingQueue(int boyut) { veri = new Object[boyut]; } // add() ve remove() sonraki slaytta }
Koşul Senkronizasyonu · Condition
BlockingQueue: Condition ile
public void add(Object oge) throws InterruptedException { kilit.lock(); try { while (buyukluk == veri.length) doluDegil.await(); veri[son] = oge; son = (son + 1) % veri.length; buyukluk++; bosDegil.signal(); } finally { kilit.unlock(); } }
public Object remove() throws InterruptedException { kilit.lock(); try { while (buyukluk == 0) bosDegil.await(); Object oge = veri[bas]; bas = (bas + 1) % veri.length; buyukluk--; doluDegil.signal(); return oge; } finally { kilit.unlock(); } }
1 2 3 4
Ekleyen yalnızca bosDegil koşulunu, çıkaran yalnızca doluDegil koşulunu uyarır: gereksiz uyandırma yok, bu yüzden signal() yeterli.
Koşul Senkronizasyonu
Hazır Çözüm: java.util.concurrent
BlockingQueue<Integer> kuyruk =
new ArrayBlockingQueue<>(2);
Thread uretici = new Thread(() -> {
try {
// put(): kuyruk doluysa bekler
for (int i = 1; i <= 4; i++) kuyruk.put(i);
} catch (InterruptedException e) { }
});
uretici.start();
for (int i = 1; i <= 4; i++) {
// take(): kuyruk boşsa bekler
System.out.print(kuyruk.take() + " ");
}
System.out.println();
uretici.join();1 2 3 4
| Yol | Bekleme | Uyandırma |
|---|---|---|
synchronized | wait() | notify(), notifyAll() |
Lock + Condition | await() | signal(), signalAll() |
ArrayBlockingQueue | put(), take() içinde | otomatik |
Gerçek projelerde hazır java.util.concurrent.BlockingQueue gerçeklemeleri (ArrayBlockingQueue, LinkedBlockingQueue) tercih edilir; kendi sınıfımız mekanizmayı anlamak içindir.
Senkronizasyon Nesneleri
Senkronizasyon Nesneleri
Çoklu iş parçacıkları arasında düzeni sağlayan hazır araçlar (java.util.concurrent); her biri farklı bir senaryoya çözüm sunar.
“N olay olsun, sonra devam et.” Bekleyenler sayaç sıfırlanana kadar durur. Tek kullanımlık.
“Hepimiz buluşma noktasına gelelim, sonra birlikte devam edelim.” Yeniden kullanılabilir.
İki iş parçacığı bir noktada buluşup veri takası yapar.
N tane izin: aynı anda en çok N iş parçacığı kaynağa girebilir.
Senkronizasyon Nesneleri · Gerçek bir çalıştırma
CountDownLatch
Sayaç kadar olayı bekleyen iş parçacıklarını senkronize eder. counter: başlangıç sayısı, burada 3.
Üç iş parçacığı başlatılır; ne zaman çalışacakları belli değil.
await(): sayaç 0 olana kadar main bekler.
1. iş parçacığı yazar; countDown() sayacı 1 azaltır: 2.
Bu çalıştırmada sıradaki 3. iş parçacığıydı: sayaç 1.
Son olay: sayaç 0. Mandal açılır, main uyanır.
Bu satır her zaman en sonda yazılır; olay satırlarının sırası ise değişebilir.
public static void main(String[] args) throws Exception { int sayac = 3; CountDownLatch mandal = new CountDownLatch(sayac); for (int i = 0; i < sayac; i++) { int no = i + 1; // lambda için kopya new Thread(() -> { System.out.println(no + ". olay gerçekleşti"); // Olay gerçekleştiğinde sayaç azaltılır. mandal.countDown(); }).start(); } // iş parçacıklarının tamamlanması beklenir. mandal.await(); System.out.println("Tüm olaylar tamamlandı!"); }
Tek kullanımlıktır: sayaç asla artmaz; sıfırken countDown() etkisizdir.
Senkronizasyon Nesneleri
CyclicBarrier
public static void main(String[] args) { int parti = 3; Runnable eylem = () -> { System.out.println("Parti tamamlandı!"); }; CyclicBarrier bariyer = new CyclicBarrier(parti, eylem); for (int i = 0; i < parti; i++) { int no = i + 1; new Thread(() -> { try { System.out.println(no + " bekliyor"); // iş parçacıklarını bekle bariyer.await(); System.out.println(no + " devam ediyor"); } catch (Exception e) { e.printStackTrace(); } }).start(); } }
- Bir grup iş parçacığını buluşma noktasında (barrier) bekletir; parti: beklenecek sayı.
await(): parti dolana kadar bekle. Dolunca bariyer eylemi bir kez çalışır, herkes devam eder.- İstisna, zaman aşımı ya da kesme bariyeri kırar: bekleyenler
BrokenBarrierExceptionalır.
1 bekliyor 2 bekliyor 3 bekliyor Parti tamamlandı! 3 devam ediyor 1 devam ediyor 2 devam ediyor
Senkronizasyon Nesneleri
Exchanger
static void takas(Exchanger<String> ex, String veri, int ms) { try { System.out.println("gönderiliyor: " + veri); Thread.sleep(ms); String alinan = ex.exchange(veri); // karşıyı bekle System.out.println(veri + " sahibi aldı: " + alinan); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { Exchanger<String> exchanger = new Exchanger<>(); // İlk iş parçacığı "Veri 1" gönderir, 2 sn sonra new Thread(() -> takas(exchanger, "Veri 1", 2000)).start(); // İkinci iş parçacığı "Veri 2" gönderir, 1 sn sonra new Thread(() -> takas(exchanger, "Veri 2", 1000)).start();
gönderiliyor: Veri 2 gönderiliyor: Veri 1 Veri 1 sahibi aldı: Veri 2 Veri 2 sahibi aldı: Veri 1
- İki iş parçacığının veri alışverişi için kullanılır; kilitten çok bir iletişim nesnesidir.
exchange(veri), karşı taraf daexchangeçağırana kadar bekler: “Veri 2” sahibi 1 sn bekledi.- Buluşunca her biri ötekinin verisini alır.
Senkronizasyon Nesneleri · Gerçek bir çalıştırma
Semaphore
Semafor, eşzamanlı erişimi sınırlar: burada aynı anda en çok 3 iş parçacığı. new Semaphore(n, true): adil dağıtım.
Beş iş parçacığı başlatılır. acquire() izin yoksa bekler, release() izni geri verir.
1 izin alır ve girer.
3 girer.
2 girer. İzin kalmadı.
4 ve 5 acquire()'da bekler.
2 sn sonra 2 çıkar ve iznini bırakır.
1 de çıkar.
Bekleyen 4 ve 5 izinleri alıp girer.
3, 4 ve 5 de çıkar; 3 izin geri döndü.
public static void main(String[] args) { int izin = 3; Semaphore semaphore = new Semaphore(izin); for (int i = 0; i < 5; i++) { int no = i + 1; new Thread(() -> { try { semaphore.acquire(); // izin al System.out.println(no + " girdi"); Thread.sleep(2000); System.out.println(no + " çıkıyor"); semaphore.release(); // izni bırak } catch (InterruptedException e) { e.printStackTrace(); } }).start(); } }
Diğer Araçlar
Atomik Sınıflar
public class AtomikSayac { public static void main(String[] args) throws InterruptedException { AtomicInteger sayac = new AtomicInteger(0); Runnable r = () -> { for (int i = 0; i < 100_000; i++) { sayac.incrementAndGet(); // atomik ++sayac } }; Thread a = new Thread(r), b = new Thread(r); a.start(); b.start(); a.join(); b.join(); System.out.println("Sonuç: " + sayac.get()); boolean b1 = sayac.compareAndSet(200000, 0); System.out.println(b1 + " " + sayac.get()); boolean b2 = sayac.compareAndSet(5, 10); System.out.println(b2 + " " + sayac.get()); } }
Sonuç: 200000 true 0 false 0
AtomicInteger,AtomicLong,AtomicBoolean…: kilitsiz, güvenli erişim.incrementAndGet()= atomik++x; işlem yarıda kesilemez.- Atomik nesne ≈
volatilealan + atomikcompareAndSet. compareAndSet(beklenen, yeni): değer hâlâ beklenen ise yeni'yi yazar,truedöner; değilse dokunmaz.
Diğer Araçlar
Kesme (Interrupting)
public class KesmeOrnegi { public static void main(String[] args) throws InterruptedException { Thread isci = new Thread(() -> { try { while (!Thread.interrupted()) { // İş parçacığının çalışma mantığı System.out.println("çalışıyor..."); Thread.sleep(400); } } catch (InterruptedException e) { System.out.println("İş parçacığı iptal edildi!"); } }); isci.start(); Thread.sleep(1000); isci.interrupt(); // sleep() içindeyken kes isci.join(); System.out.println("main: bitti"); } }
çalışıyor... çalışıyor... çalışıyor... İş parçacığı iptal edildi! main: bitti
interrupt(): kesme isteği; iş parçacığısleep/wait'teyseInterruptedException.Thread.interrupted()bayrağı okur ve temizler;isInterrupted()yalnızca okur.
Diğer Araçlar
Askıya Alma (Suspension)
class Gorev implements Runnable { private boolean duraklatildi = false; public synchronized void duraklat() { duraklatildi = true; } public synchronized void devamEt() { duraklatildi = false; notifyAll(); // bekleyeni uyandır } public void run() { try { for (int i = 1; i <= 5; i++) { synchronized (this) { while (duraklatildi) wait(); } System.out.println("adım " + i); Thread.sleep(200); } } catch (InterruptedException e) { } } }
Gorev g = new Gorev(); Thread t = new Thread(g); t.start(); Thread.sleep(500); g.duraklat(); System.out.println("main: duraklatıldı"); Thread.sleep(1000); System.out.println("main: devam"); g.devamEt(); t.join();
adım 1 adım 2 adım 3 main: duraklatıldı main: devam adım 4 adım 5
suspend()/resume() yerine wait()/notifyAll().
Diğer Araçlar
ThreadLocal Sınıfı
static final ThreadLocal<String> deger = new ThreadLocal<>(); public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() -> { // İlk iş parçacığı deger.set("Değer 1"); System.out.println("thread1: " + deger.get()); }); Thread thread2 = new Thread(() -> { // İkinci iş parçacığı deger.set("Değer 2"); System.out.println("thread2: " + deger.get()); }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("main: " + deger.get());
thread1: Değer 1 thread2: Değer 2 main: null
thread2: Değer 2 thread1: Değer 1 main: null
- Her iş parçacığının kendi ayrı kopyası olan değişken; kopyalar birbirinden bağımsızdır.
- main hiç
setetmediği içinnullgörür. ThreadLocal.withInitial(() -> 0)(Java 8+) ya dainitialValue()'yu geçersiz kılmak: her iş parçacığına ayrı başlangıç değeri.
Diğer Araçlar
İş Parçacığı Havuzları (Thread Pools)
- Kullanılacak iş parçacıklarını önceden oluşturup yöneten yapı: görevler havuza gönderilir (submit).
- İş parçacığı oluşturmak pahalıdır; havuz iş parçacıklarını yeniden kullanır ve sayılarını sınırlar.
ExecutorServicearayüzü havuzdaki işleri kontrol eder;Executors.newFixedThreadPool(n)hazır havuz verir.ThreadPoolExecutor: havuzu oluşturup yöneten, ayrıntılı ayarlanabilen sınıf.
Diğer Araçlar
ExecutorService
public class HavuzOrnegi { static void gorev(int no) { String ad = Thread.currentThread().getName(); System.out.println("Görev " + no + " -> " + ad); } public static void main(String[] args) throws Exception { ExecutorService havuz = Executors.newFixedThreadPool(2); for (int i = 1; i <= 4; i++) { int no = i; havuz.submit(() -> gorev(no)); } // Callable: değer döndüren görev Future<Integer> f = havuz.submit(() -> 6 * 7); System.out.println("Callable sonucu: " + f.get()); havuz.shutdown(); // yeni görev kabul etme havuz.awaitTermination(5, TimeUnit.SECONDS); System.out.println("Havuz kapandı"); } }
Java 21+: Executors.newVirtualThreadPerTaskExecutor() her göreve hafif bir sanal iş parçacığı (virtual thread) verir.
Görev 1 -> pool-1-thread-1 Görev 2 -> pool-1-thread-2 Görev 3 -> pool-1-thread-2 Görev 4 -> pool-1-thread-2 Callable sonucu: 42 Havuz kapandı
Görev 3 -> pool-1-thread-1 Görev 4 -> pool-1-thread-2
Callable değer döndürür; Future.get() sonucu bekler. shutdown() unutulursa havuzun iş parçacıkları programı açık tutar.
Diğer Araçlar
İş Parçacığı Güvenliği (Thread Safety)
Bir sınıf, birden çok iş parçacığından aynı anda kullanıldığında da doğru davranıyorsa iş parçacığı güvenlidir.
Oluşturulduktan sonra değişmeyen nesneler (String, final alanlı sınıflar, Java 16+ record) senkronizasyonsuz paylaşılabilir.
Yerel değişkenler her iş parçacığının yığınındadır; ThreadLocal da kopya verir. Paylaşılmayan veri yarışmaz.
Paylaşılan değişen veriye her erişim aynı kilitle korunur: synchronized, Lock.
AtomicInteger, ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue; ArrayList ve HashMap güvenli değildir.
Özet
Özet
| Konu | Ana fikir | Araç |
|---|---|---|
| İş parçacığı | Süreç içinde bağımsız yürütme akışı; heap ortak, yığın ayrı | Thread, Runnable |
| Başlatma | İş run()'da; başlatmak için start() | start(), join() |
| Yaşam döngüsü | NEW → RUNNABLE ⇄ BLOCKED / WAITING / TIMED_WAITING → TERMINATED | getState() |
| Durdurma | İş birliğiyle: bayrak ya da kesme | volatile, interrupt() |
| Yarış durumu | Paylaşılan veride bölünebilir işlemler; sonuç sıraya bağlı | synchronized, Lock |
| Görünürlük | Bir iş parçacığının yazdığı diğerine görünmeyebilir | volatile |
| Kilitlenme | Kilitleri farklı sırada almak döngü yaratır | Sabit sıra, tryLock |
| Koşul bekleme | Kilidi bırakarak bekle, değişince uyandır | wait/notify, Condition |
| Koordinasyon | Sayma, buluşma, takas, izin | CountDownLatch, CyclicBarrier, Exchanger, Semaphore |
| Diğer | Kilitsiz sayaç, iş parçacığına özel değer, havuz | AtomicInteger, ThreadLocal, ExecutorService |
Özet
İlgili Sayfalar
Süreç ve iş parçacığı, eşzamanlılık ile paralellik, senkronizasyon, kilitlenme, semafor, atomik işlemler, volatile ve yanlış paylaşım (false sharing) kavramlarının kısa ve kod örnekli özeti.
Pekiştirme
Sık Yapılan Hatalar (1)
| Hata | Belirti | Doğrusu |
|---|---|---|
t.run() çağırmak | Yeni iş parçacığı yok; kod main'de sırayla çalışır | t.start() |
start()'ı iki kez çağırmak | IllegalThreadStateException | Her çalıştırma için yeni Thread |
Thread.sleep(100); yalnız başına | error: unreported exception InterruptedException; must be caught or declared to be thrown | try-catch ya da throws InterruptedException |
Lambda içinde döngü değişkeni i | error: local variables referenced from a lambda expression must be final or effectively final | int no = i; kopyası |
Fırlatılmayan istisnayı yakalamak: try { while (!Thread.interrupted()) { } } catch (InterruptedException e) | error: exception InterruptedException is never thrown in body of corresponding try statement | Gövdede sleep/wait olmalı ya da catch kaldırılmalı |
Sınıf Thread'i genişletmiyorken currentThread() | error: cannot find symbol | Thread.currentThread() |
join()'u unutmak | main, sonuç hazır olmadan okur (ör. 0) | Sonucu okumadan önce join() |
Pekiştirme
Sık Yapılan Hatalar (2)
| Hata | Belirti | Doğrusu |
|---|---|---|
wait()/notify()'ı synchronized dışında çağırmak | IllegalMonitorStateException: current thread is not owner | Aynı nesne üzerinde synchronized içinde |
Koşulu if ile beklemek | Uyanınca koşul yine bozuk olabilir; taşan/boş kuyruk | while (koşul) wait(); |
unlock()'u finally dışında yazmak | İstisnada kilit hiç açılmaz; herkes sonsuza dek bekler | lock(); try { … } finally { unlock(); } |
| Almadığı kilidi açmak | IllegalMonitorStateException | Her lock() için bir unlock() |
volatile sayaçta sayac++ | Artışlar kaybolur (ör. 163299) | AtomicInteger ya da synchronized |
final volatile alan | error: illegal combination of modifiers: final and volatile | Birini seçin |
stop(), suspend() kullanmak | Java 20+: UnsupportedOperationException | Bayrak, interrupt(), wait/notify |
InterruptedException'ı boş catch ile yutmak | Kesme isteği kaybolur, iş parçacığı durmaz | Döngüden çık ya da Thread.currentThread().interrupt() |
| Kilitleri farklı sırada almak | Kilitlenme: program donar | Herkes aynı sırayla alsın |
Pekiştirme
Örnek Problemler (1)
1) Çıktı nedir? Her çalıştırmada aynı mı?
Thread t = new Thread(() -> System.out.print("B"));
System.out.print("A");
t.start();
t.join();
System.out.print("C");2) Çıktı nedir?
Thread t = new Thread(() -> System.out.print("X"));
System.out.print(t.getState() + " ");
t.run();
System.out.print(" " + t.getState());3) Aşağıdakilerden hangileri “İki İş Parçacığı” örneğinin olası çıktısıdır?
a) A1 A2 B1 B2 main b) B1 A1 A2 B2 main
c) A1 main B1 A2 B2 d) A2 A1 B1 B2 main
Cevaplar
ABC, her zaman.Astart'tan önce,Byeni iş parçacığında;join()B bitmedenC'ye geçirmez.NEW X NEW.run()sıradan çağrıdır: X'i main yazar, iş parçacığı hiç başlamaz,NEWkalır.- a ve b. c olamaz: main iki
join()'den sonra yazar. d olamaz: aynı iş parçacığının adımları kendi içinde sırasını korur.
Pekiştirme
Örnek Problemler (2)
4) Her durumda iş parçacığının durumu ne?
a) sleep(500) içinde · b) dolu bir synchronized bloğa girerken · c) wait() sonrası · d) run() bitince
5) İş parçacığı güvenli mi?
class Hesap { private int bakiye = 0; void yatir(int m) { bakiye += m; } int getBakiye() { return bakiye; } }
6) İki iş parçacığı bunları çalıştırıyor. Ne olabilir?
// T1 // T2 synchronized (a) { synchronized (b) { synchronized (b) { } synchronized (a) { } } }
Cevaplar
- a)
TIMED_WAITINGb)BLOCKEDc)WAITINGd)TERMINATED. - Hayır:
bakiye += moku–ekle–yaz'dır. Metotlarısynchronizedyapın ya daAtomicInteger.addAndGet(m). - Kilitlenme: T1
a'yı, T2b'yi alırsa ikisi de ötekini sonsuza dek bekler. Çözüm: ikisi de öncea, sonrab.
Pekiştirme
Alıştırmalar (1)
- 1. 1..1000 sayılarının toplamını iki iş parçacığıyla hesaplayın: biri ilk yarıyı, öteki ikinci yarıyı toplasın; main ikisini bekleyip toplamı yazsın.
- 2. 1..1000 arasındaki çift sayıları dört iş parçacığıyla (her biri 250 sayı) ortak bir sayaçta sayın. Sayaç doğru çıkmalı.
Cevaplar
1. Her iş parçacığı kendi gözüne yazar; join() sonrası okuma güvenlidir. Çıktı: 500500
Thread sol = new Thread(() -> { for (int i = 0; i < 500; i++) parca[0] += dizi[i]; }); Thread sag = new Thread(() -> { for (int i = 500; i < 1000; i++) parca[1] += dizi[i]; }); sol.start(); sag.start(); sol.join(); sag.join();
2. AtomicInteger cift; her iş parçacığı for (int i = bas; i < bas + 250; i++) if (i % 2 == 0) cift.incrementAndGet(); — bas = 1, 251, 501, 751. Tüm join()'lerden sonra çıktı: Çift sayı: 500
Pekiştirme
Alıştırmalar (2)
- 3. 1..5 sayılarının karelerini 3 iş parçacıklı bir havuzda
Callablegörevleriyle hesaplayın veFuture'lardan toplayın. - 4. Her saniye “tik” yazan bir iş parçacığı yazın; main 3,5 sn sonra onu
interrupt()ile durdursun.
Cevaplar
ExecutorService havuz = Executors.newFixedThreadPool(3); List<Future<Long>> sonuclar = new ArrayList<>(); for (int i = 1; i <= 5; i++) { int n = i; sonuclar.add(havuz.submit(() -> kare(n))); } long toplam = 0; for (Future<Long> f : sonuclar) toplam += f.get(); havuz.shutdown(); System.out.println("Karelerin toplamı: " + toplam);
3. static long kare(int n) { return (long) n * n; } yardımcı metodu ile. Çıktı: Karelerin toplamı: 55
4. “Kesme” slaytındaki kalıp; döngüde sleep(1000), main içinde sleep(3500) ve interrupt(). Çıktı: tik · tik · tik · tik · saat durdu
Özet
Kaynaklar
- Oracle, The Java Tutorials: Lesson: Concurrency (Processes and Threads, Synchronization, Liveness, Guarded Blocks, High Level Concurrency Objects).
- J. Gosling ve diğ., The Java Language Specification, Java SE 21 Edition, Bölüm 17: Threads and Locks.
- Oracle, Java SE 21 API Documentation:
java.lang.Thread,java.util.concurrent,java.util.concurrent.locks,java.util.concurrent.atomic. - B. Goetz ve diğ., Java Concurrency in Practice, Addison-Wesley, 2006.
- P. Deitel ve H. Deitel, Java: How to Program, 11. baskı, Pearson (Bölüm 23: Concurrency).
- C. S. Horstmann, Core Java, Volume I: Fundamentals, 12. baskı, Pearson (Bölüm 12: Concurrency).
- K. Sierra, B. Bates ve T. Gee, Head First Java, 3. baskı, O'Reilly (iş parçacıkları ve eşzamanlılık bölümleri).
- J. Bloch, Effective Java, 3. baskı, Addison-Wesley (Bölüm 11: Concurrency, Madde 78–84).