Bölüm 9: İş Parçacıkları

Java ile Nesne Yönelimli Programlama
İş parçacığı ve süreçThread ve RunnableYaşam döngüsüsleep · join · daemon DurdurmaYarış durumusynchronizedvolatileLockKilitlenme wait · notify · ConditionSenkronizasyon nesneleriAtomik sınıflarThreadLocalİş parçacığı havuzu
Sercan KÜLCÜ · Giresun Üniversitesi · Elektrik-Elektronik Mühendisliği

Giriş

Öğrenme Çıktıları

Kavramlar

İş parçacığı ile süreç, eşzamanlılık ile paralellik arasındaki farkı açıklar.

Oluşturma

Runnable gerçekleyerek ya da Thread sınıfını genişleterek iş parçacığı yazar, start() ile başlatır.

Yaşam döngüsü

Altı durumu ve sleep, join, wait, kilit beklemesi gibi geçişleri izler.

Senkronizasyon

Yarış durumunu tanır; synchronized, volatile, Lock ve atomik sınıflarla önler.

Koşul bekleme

wait/notify ve Condition ile üretici–tüketici yazar, kilitlenmeyi önler.

Hazır araçlar

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 Thread sı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).
Benzetme: arı kovanı

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.

Neden?

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

Tek çekirdekeşzamanlılıkABABABABABİki çekirdekparalellikÇ1AÇ2Bzaman
Eşzamanlılık (concurrency)

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.

Paralellik (parallelism)

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ç: çalışan bir JVMHeap: nesneler ve dizilertüm iş parçacıkları ORTAK kullanırmainYığın (stack)yerel değişkenlerPC: sıradaki komutThread-0Yığın (stack)yerel değişkenlerPC: sıradaki komut
Süreç (process)İş parçacığı
BellekKendi adres alanıSürecin heap'ini paylaşır
Yığın—Her birinin kendi yığını
OluşturmaPahalıUcuz (hafif süreç)
İletişimDosya, soket, boruOrtak nesneler
RiskBirbirini bozamazOrtak veriyi bozabilir
Çalışan bir Java programı bir süreçtir; içinde birçok iş parçacığı çalışır. Nesneler heap'te olduğu için iki iş parçacığı aynı nesneye erişebilir; yerel değişkenler ise her iş parçacığının kendi yığınındadır ve paylaşılmaz.

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.

Sınıf Yükleyici (Class Loader)JVM bellek alanlarıSınıf alanı(method area)HeapnesnelerYığın(stack)PC yazmacı(PC register)Yerel metotyığınıYürütme Motoru(yorumlayıcı, JIT, GC)Yerel Metot Arayüzü(JNI)YerelkütüphanelerOrtak: tüm iş parçacıkları paylaşırHer iş parçacığına ayrı

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.lang paketindeki Thread sı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.
MetotGö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)

Runnable (çalıştırılabilir)

run() metodunu içeren nesne. İş parçacığının ana mantığı burada tanımlanır.

Name (isim)

İş parçacığının adı: main, Thread-0… Günlük kayıtları (log) ve teşhis için kullanılır. getName(), setName()

ID (kimlik)

Tekil kimlik numarası; iş parçacığı oluşturulurken sistem tarafından otomatik atanır. threadId()

ThreadGroup (grup)

İş parçacığının ait olduğu grup; iş parçacıklarını düzenlemek ve yönetmek için kullanılır.

Daemon (arka plan)

Diğer iş parçacıklarına hizmet eder, belirli görevleri periyodik yapar; JVM kapanırken bitmesi beklenmez.

ContextClassLoader

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)

Priority (öncelik)

MIN_PRIORITY (1) ile MAX_PRIORITY (10) arasında tamsayı; varsayılan NORM_PRIORITY (5).

State (durum)

Mevcut durum: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED.

Interrupted (kesilme)

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());
Çıktı
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

system grubuReference HandlerFinalizerSignal DispatcherAttach Listener …main grubumainThread-0, Thread-1 …kullanıcıJVM'in daemon iş parçacıkları
Main thread group

JVM başlarken oluşturulur; ana iş parçacığı ve bizim oluşturduklarımız varsayılan olarak buraya aittir.

System thread group

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());
}
Çıktı (JDK 21, bir çalıştırma)
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ığı

Main Thread (ana iş 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.

Garbage Collector (çöp toplayıcı)

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.

Signal Dispatcher (sinyal gönderici)

İşletim sisteminden gelen sinyalleri yönetir; örneğin Ctrl+C ile gelen kapatma isteğinde JVM'in güvenli kapanmasını başlatır.

Finalizer (sonlandırıcı)

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.

DurumAnlamı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ı bekliyorstart() sonrası
BLOCKED (engellenmiş)synchronized blok/metoda girmek için monitör kilidini bekliyorKilit başkasındayken
WAITING (bekliyor)Süresiz bekliyor; başka bir iş parçacığı uyandırmalıObject.wait(), Thread.join(), LockSupport.park()
TIMED_WAITINGSüreli bekliyorThread.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ı

NEWRUNNABLETERMINATEDBLOCKEDWAITINGTIMED_WAITINGstart()run() bitersynchronized kilidi bekler⇄ kilidi alırwait(), join()⇄ notify(), iş bitersleep(ms), wait(ms)⇄ süre dolar
Tek yön

NEW → RUNNABLE → TERMINATED. Biten iş parçacığı yeniden başlatılamaz.

Bekleme durumları

Üç bekleme durumunun her birinden yeniden RUNNABLE'a dönülür.

Fark

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

mainRUNNABLEuyuyanNEWbekleyenhenüz yokengellenenhenüz yok

uyuyan nesnesi oluşturuldu ama başlatılmadı: NEW.

mainRUNNABLEuyuyanNEWbekleyenhenüz yokengellenenhenüz yok

Durum yazdırılır.

mainRUNNABLEuyuyanRUNNABLEbekleyenhenüz yokengellenenhenüz yok

start(): yeni iş parçacığı çalışmaya hazır.

mainTIMED_WAITINGuyuyanTIMED_WAITINGbekleyenhenüz yokengellenenhenüz yok

İkisi de sleep içinde: main 100 ms, uyuyan 1000 ms.

mainRUNNABLEuyuyanTIMED_WAITINGbekleyenhenüz yokengellenenhenüz yok

main uyandı; uyuyan hâlâ uyuyor: TIMED_WAITING.

mainRUNNABLEuyuyanTIMED_WAITINGbekleyenWAITINGengellenenhenüz yok

bekleyen, uyuyan.join() çağırır: süresiz bekler, WAITING.

mainRUNNABLEuyuyanTIMED_WAITINGbekleyenWAITINGengellenenhenüz yok

100 ms sonra durum yazdırılır.

mainRUNNABLEuyuyanTIMED_WAITINGbekleyenWAITINGengellenenBLOCKED

main kilit'i tutuyor; engellenen aynı kilide girmek istiyor: BLOCKED.

mainRUNNABLEuyuyanTIMED_WAITINGbekleyenWAITINGengellenenBLOCKED

Kilit hâlâ main'de; durum yazdırılır.

mainWAITINGuyuyanTERMINATEDbekleyenTERMINATEDengellenenTERMINATED

Blok bitince kilit bırakılır, engellenen girip biter. main bekleyen'i bekler; uyuyan 1 sn dolunca biter, bekleyen de biter.

mainRUNNABLEuyuyanTERMINATEDbekleyenTERMINATEDengellenenTERMINATED

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 Runnable nesneye sahiptir.
  • Runnable arayüzü yalnızca run() 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");
    }
}
  • Thread yapıcısına parametre olarak Runnable nesne 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.
Çıktı (gerçek bir çalıştırma)
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ış

mainmain()RUNNABLEThread-0yığın yok

main çalışıyor; Worker nesnesi heap'te oluşturulur. Henüz tek iş parçacığı var.

mainmain()RUNNABLEThread-0yığın yokNEW

Thread nesnesi oluştu (NEW) ama henüz ayrı bir yürütme akışı ve yığın yok.

mainmain()RUNNABLEThread-0run()RUNNABLE

start(): JVM yeni bir yığın açar ve orada run()'ı başlatır. start() hemen döner.

mainmain()RUNNABLEThread-0run()RUNNABLE (sıra bekliyor)

Bir olası sıra: main önce davranır ve yazdırır.

mainboşTERMINATEDThread-0run()RUNNABLE

main() bitti ama program bitmedi: JVM, daemon olmayan iş parçacıkları bitene kadar bekler.

mainboşTERMINATEDThread-0run()RUNNABLE

Thread-0 kendi yığınında çalışır; ad onun yerel değişkenidir.

mainboşTERMINATEDThread-0run()RUNNABLE

Thread-0 yazdırır.

mainboşTERMINATEDThread-0boşTERMINATED

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ığı, Thread sınıfı genişletilerek (extends) de oluşturulabilir.
  • Runnable gerçeklemek yerine Thread alt sınıfı yazılır; Thread zaten Runnable'ı gerçekler.
  • Nesnenin kendisi bir Thread olduğu için doğrudan w.start() ve getName() çağrılır.
Çıktı (gerçek bir çalıştırma)
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();
    }
}
Çıktı
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 kezIllegalThreadStateException: bir Thread nesnesi yalnızca bir kez başlatılabilir.
Kural: iş parçacığının işini 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, Thread sınıfını genişletemez: extends Ata, Thread derlenmez.
  • Çözüm: Runnable arayüzünü gerçekle ve sınıf içinde Thread türünde bir alan tut.
  • Arayüz sayısında sınır yoktur; bu yüzden Runnable genellikle tercih edilir.
Ata+ selamla(): void«interface»Runnable+ run(): voidWorker- thread: Thread+ run(): void+ start(): voidextendsimplements

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
    }
}
Çıktı
Ata sınıfından merhaba
run(): Thread-0
  • Worker hem Ata'nın metotlarını kalıtır hem de bir iş parçacığında çalışabilir.
  • Kendi start() metodumuz new Thread(this) ile kendini bir Thread'e verir.
  • this, Runnable türündendir; Thread yapı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();
    }
}
Çıktı
1) Runnable sınıfı
2) Thread alt sınıfı
3) Lambda
YolNe zaman?
implements RunnableSınıf başka sınıftan türemişse; iş ile iş parçacığı ayrı tutulur
extends ThreadKı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

mainAB▶azaman →

main, A'yı başlatır. A artık RUNNABLE; ne zaman çalışacağı belli değil.

mainAB▶a▶bzaman →

main, B'yi başlatır.

mainAB▶a▶bzaman →

a.join(): main, A bitene kadar WAITING. Artık A ve B çalışır.

mainAB▶a▶b1zaman →

Zamanlayıcı A'yı çalıştırır: A kendi yığınında gorev()'i yürütür.

mainAB▶a▶b11zaman →

Bağlam değişimi (context switch): şimdi B çalışıyor. B'nin i'si ayrı bir yerel değişkendir.

mainABjoin() bekler▶a▶b112zaman →

Tekrar A.

mainABjoin() bekler▶a▶b1122zaman →

Tekrar B. Ardından ikisi de biter (TERMINATED).

mainABjoin() bekler▶a▶b1122yazzaman →

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

sleep(ms)

Çalışan iş parçacığını belirli bir süre uyutur (TIMED_WAITING). Kilit tutuyorsa bırakmaz.

join()

Çağıran iş parçacığı, hedef iş parçacığı bitene kadar bekler. join(ms): en çok ms kadar.

yield()

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);
    }
}
Çıktı
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");
    }
}
Çıktı
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.
  • saat sonsuz döngüde olsa da main bitince program sonlanır.
  • setDaemon(true), start()'tan önce çağrılmalı; sonra çağrılırsa IllegalThreadStateException.

Yönetim ve Durdurma

İş Parçacığını Durdurma

  • İş parçacığı, run() metodu tamamlandığında durur.
  • JVM kapanır: System.exit() (içeride Runtime.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 volatile tanımlanır.
  • Alternatif: interrupt() ile kesme isteği (ileride).
Thread.stop() kullanmayın

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");
}
Çıktı (JDK 21)
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");
    }
}
Çıktı
ç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 ve run()'dan kendisi çıkar.
  • main 350 ms uyurken işçi 0, 100, 200, 300 ms'de dört kez yazdı.
  • volatile olmasaydı 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");
    }
}
Çıktı
run() bitti
main: bitti
  • Sınıf Runnable gerçekler; çalışan Thread nesnesi volatile bir 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ı null yapar; koşul bozulur, run() biter.
  • currentThread() static bir Thread metodudur; Thread'i genişletmeyen sınıfta Thread. ön eki şarttır.

Senkronizasyon

İşlemleri Doğru Sırayla Gerçekleştirmek

Hız belirsiz

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.

Bayat değer

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.

Yeniden sıralama

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.

Paylaşılan ve değişen veriye birden fazla iş parçacığı erişiyorsa erişim senkronize edilmelidir. Aksi hâlde sonuç, iş parçacıklarının hangi sırayla çalıştığına bağlı olur: buna yarış durumu (race condition) denir.

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);
    }
}
1. çalıştırma
Beklenen: 200000
Bulunan : 138001
2. çalıştırma
Beklenen: 200000
Bulunan : 135352
3. çalıştırma
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?

belleksayac = 0t1yazmaç = —t2yazmaç = —

Başta sayac = 0. İki iş parçacığı da sayac++ yapacak; doğru sonuç 2 olmalı.

belleksayac = 0t1yazmaç = 0t2yazmaç = —oku

t1 okur: yazmacına 0 gelir.

belleksayac = 0t1yazmaç = 0t2yazmaç = 0oku

Bağlam değişimi! t2 de okur: o da 0 görür.

belleksayac = 0t1yazmaç = 1t2yazmaç = 0

t1 kendi yazmacında 1 ekler.

belleksayac = 1t1yazmaç = 1t2yazmaç = 0yaz

t1 yazar: sayac = 1.

belleksayac = 1t1yazmaç = 1t2yazmaç = 1

t2 kendi (eski) değerine 1 ekler: 1.

belleksayac = 1t1yazmaç = 1t2yazmaç = 1yaz

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

join()

Bir iş parçacığının bitmesini beklemek.

synchronized

Nesnenin monitör kilidiyle karşılıklı dışlama.

Lock nesneleri

ReentrantLock, ReadWriteLock: açık kilitleme.

Bariyer, semafor, değiştirici

CyclicBarrier, Semaphore, Exchanger, CountDownLatch.

volatile değişkenler

Okuma/yazmanın tüm iş parçacıklarınca görünmesi.

Atomik nesneler

AtomicInteger gibi bölünemez işlemler.

Senkronizasyona benzeyen ama olmayanlar: öncelikler (priority), uyutma (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 synchronized ile 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
    }
};
Çıktı (2 × 50000, her seferinde)
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ı.

giriş kümesiBLOCKEDo nesnesimonitörsahibi: yokbittit1t2

t1 ve t2 aynı anda synchronized (o) satırına gelir; monitör boştur.

giriş kümesiBLOCKEDo nesnesimonitörsahibi: t1bittit2t1

t1 kazanır ve monitörü alır. t2 giriş kümesinde BLOCKED bekler.

giriş kümesiBLOCKEDo nesnesimonitörsahibi: yokbittit2t1

t1 bloktan çıkar: monitör otomatik bırakılır. Bekleyenler rekabet eder.

giriş kümesiBLOCKEDo nesnesimonitörsahibi: t2bittit2t1

t2 monitörü alır ve kritik bölgeye girer.

giriş kümesiBLOCKEDo nesnesimonitörsahibi: yokbittit1t2

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 synchronized tanımlamak, gövdesini bir synchronized bloğa almakla aynıdır.
  • Örnek metot → this nesnesinin monitörü.
  • static metot → sınıfın C.class nesnesinin monitörü (nesne yok).
  • Bu yüzden bir static synchronized ile bir örnek synchronized metot 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());
    }
}
Çıktı (her çalıştırmada)
Sonuç: 200000
  • arttir() artık synchronized: 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);
}
Çıktı
[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.
  • long ve double dışındaki değişkenlerin tek okuma/yazması zaten atomiktir; volatile long/double da atomik olur.
  • Paylaşılan basit bir bayrak için synchronized ve kilit gerektirmez.
  • Ama yalnızca okuma ve yazma atomiktir: x++ gibi oku–değiştir–yaz işlemi atomik değildir.
Çekirdek 1: mainönbellek: dur = trueÇekirdek 2: tyazmaç: dur = falseAna bellek: dur = trueyazartekrar okumaz(eski kopya)

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);
    }
}
Çıktı: volatile YOK
main: dur = true
hâlâ çalışıyor mu? true
Çıktı: static volatile boolean dur
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++;
};
Üç çalıştırma (beklenen 200000)
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;
}
Derleyici hatası
FinalVolatile.java:2: error: illegal combination of modifiers: final and volatile
    private final volatile int x = 5;
                               ^
1 error
finalvolatile
Bir kez atanır (yapıcıda); sonra değişmezDeğeri değişebilir
Yapıcı bitince herkes aynı değeri görürHer 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.locks paketindeki Lock arayüzü; ReentrantLock ve ReadWriteLock geliş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.*
Çıktı
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() finally iç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
Çalışma zamanı hatası
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 beklemeden false.
  • tryLock(3, SECONDS): en çok 3 sn bekler; InterruptedException bildirilmelidir.
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();
Çıktı
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 ReentrantLock kullanılarak iç içe kritik bölgelere erişilebilir.
  • Adil kilitleme (fair lock): new ReentrantLock(true) ile kilit, en uzun bekleyene verilir.
  • synchronized da yeniden girişlidir: synchronized bir metot aynı nesnenin başka bir synchronized metodunu ç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());
Çıktı
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

kilitsahibi: yoksayı = 0

main, dis()'i çağırır. Kilit boş.

kilitsahibi: mainsayı = 1

dis() kilidi alır: sahibi main, tutma sayısı 1.

kilitsahibi: mainsayı = 1

yaz sayıyı yazdırır.

kilitsahibi: mainsayı = 2

ic() aynı kilidi ister. Sahibi zaten main olduğu için beklemez: sayı 2 olur.

kilitsahibi: mainsayı = 2

Yazdırılır.

kilitsahibi: mainsayı = 1

ic()'nin finally'si: unlock() sayıyı 1'e indirir; kilit hâlâ main'de.

kilitsahibi: yoksayı = 0

dis()'in finally'si: sayı 0, kilit gerçekten bırakıldı.

kilitsahibi: yoksayı = 0

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ı

Okuyucu-1Okuyucu-2Okuyucu-3Yazıcıokumalar paralelyazma tek başınayine paralelzaman →
Okuma kilidi

Birden fazla iş parçacığı veriyi aynı anda okuyabilir; okurken birbirini engellemez (paralel okuma).

Yazma kilidi

Bir iş parçacığı yazarken diğerlerinin okuması da yazması da engellenir; yazma güvenli ve atomik olur.

Ne zaman?

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();
Çıktı (bir çalıştırma)
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: LockDolaylı: synchronized
KontrolGeliştirici lock()/unlock() çağırırOtomatik: blok girişinde alınır, çıkışta bırakılır
SınıflarLock arayüzü, ReentrantLock, ReentrantReadWriteLockAnahtar kelime; her nesnenin monitörü
Beklemeden denemetryLock(), tryLock(süre): engellemeden koşullu almaYok: kilit boşalana kadar bekler
Kesilebilir beklemelockInterruptibly()Yok
Adillik, koşullarnew ReentrantLock(true), birden çok ConditionYok; tek bekleme kümesi
Yeniden girişReentrantLock yeniden girişlidirHer zaman yeniden girişlidir
Riskunlock() unutulursa kilit sonsuza dek kalırUnutma riski yok
Ne zaman?Gelişmiş kontrol gerektiğindeBasit 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);
}
T1T2abtutarbeklertutarbekler
Çıktı
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"));
Çıktı
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.

Sabit kilit sırası

Tüm iş parçacıkları kilitleri aynı sırayla alsın (ör. kimlik numarasına göre).

Süreli deneme

tryLock(süre) alamazsa elindekileri bırakıp sonra tekrar dener.

Kısa kritik bölge

Kilit tutarken başka kilit istemekten ve uzun işlerden kaçının.

Teşhis

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.
Java'nın destekleri

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()

giriş kümesikilidi bekleyenlerBLOCKEDmonitör sahibi(tek iş parçacığı)bekleme kümesikoşul bekleyenlerWAITINGalırwait():bırakırnotify() / notifyAll(): uyanan, kilidi yeniden almak için sıraya girer
  • 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 synchronized içinde çağrılır; yoksa IllegalMonitorStateException: current thread is not owner.
  • Kural 2: koşul if ile değil while ile 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, son eklenecek konum; % veri.length başa sarar.
  • Doluysa add, boşsa remove bekler; her değişiklikten sonra notifyAll() bekleyenleri uyandırır.

Koşul Senkronizasyonu · Bir olası sıra

Üretici–Tüketici: Adım Adım

veri[0]1veri[1]—bassonÜreticiçalışıyorTüketiciçalışıyor

Üretici add(1): yer var, 1 eklenir; son 1'e ilerler.

veri[0]1veri[1]2bassonÜreticiçalışıyorTüketiciçalışıyor

add(2): son 2 % 2 = 0'a sarar. Kuyruk dolu.

veri[0]1veri[1]2bassonÜreticiWAITINGTüketiciçalışıyor

add(3): dolu olduğu için wait(): üretici bekleme kümesine girer ve kilidi bırakır.

veri[0]—veri[1]2bassonÜreticiWAITINGTüketiciçalışıyor

Tüketici kilidi alabildi: remove() 1'i alır, bas 1 olur.

veri[0]—veri[1]2bassonÜreticiBLOCKEDTüketiciçalışıyor

notifyAll(): üretici uyanır ama kilit hâlâ tüketicide; kilidi bekler. remove 1 döndürür.

veri[0]3veri[1]2bassonÜreticiçalışıyorTüketiciçalışıyor

Üretici kilidi alır, while koşulunu yeniden denetler: dolu değil. 3, veri[0]'a yazılır.

veri[0]3veri[1]—bassonÜreticiçalışıyorTüketiciçalışıyor

Tüketici 2'yi alır; bas başa sarar.

veri[0]—veri[1]—bassonÜreticiçalışıyorTüketiciçalışıyor

Tüketici 3'ü alır. Kuyruk boş.

veri[0]—veri[1]—bassonÜreticiçalışıyorTüketiciWAITING

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());
Çıktı (bir çalıştırma)
ü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ğini Lock dünyasına taşır; ReentrantLock ile 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();
    }
}
Çıktı: üretici 1–4 ekler, main 4 kez çıkarır
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();
Çıktı
1 2 3 4 
YolBeklemeUyandırma
synchronizedwait()notify(), notifyAll()
Lock + Conditionawait()signal(), signalAll()
ArrayBlockingQueueput(), take() içindeotomatik

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.

CountDownLatch (geri sayım mandalı)

“N olay olsun, sonra devam et.” Bekleyenler sayaç sıfırlanana kadar durur. Tek kullanımlık.

CyclicBarrier (döngüsel bariyer)

“Hepimiz buluşma noktasına gelelim, sonra birlikte devam edelim.” Yeniden kullanılabilir.

Exchanger (değiştirici)

İki iş parçacığı bir noktada buluşup veri takası yapar.

Semaphore (semafor)

N tane izin: aynı anda en çok N iş parçacığı kaynağa girebilir.

Senkronizasyon Nesneleri · Gerçek bir çalıştırma

CountDownLatch

sayaç3123olaylarmainçalışıyor

Sayaç kadar olayı bekleyen iş parçacıklarını senkronize eder. counter: başlangıç sayısı, burada 3.

sayaç3123olaylarmainçalışıyor

Üç iş parçacığı başlatılır; ne zaman çalışacakları belli değil.

sayaç3123olaylarmainWAITING (await)

await(): sayaç 0 olana kadar main bekler.

sayaç2123olaylarmainWAITING (await)

1. iş parçacığı yazar; countDown() sayacı 1 azaltır: 2.

sayaç1123olaylarmainWAITING (await)

Bu çalıştırmada sıradaki 3. iş parçacığıydı: sayaç 1.

sayaç0123olaylarmaindevam

Son olay: sayaç 0. Mandal açılır, main uyanır.

sayaç0123olaylarmaindevam

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 BrokenBarrierException alır.
Çıktı (bir çalıştırma)
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();
Çıktı
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 da exchange ç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

izinler3 / 31—2—3—4—5—

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.

izinler3 / 31hazır2hazır3hazır4hazır5hazır

Beş iş parçacığı başlatılır. acquire() izin yoksa bekler, release() izni geri verir.

izinler2 / 31içeride2hazır3hazır4hazır5hazır

1 izin alır ve girer.

izinler1 / 31içeride2hazır3içeride4hazır5hazır

3 girer.

izinler0 / 31içeride2içeride3içeride4hazır5hazır

2 girer. İzin kalmadı.

izinler0 / 31içeride2içeride3içeride4bekliyor5bekliyor

4 ve 5 acquire()'da bekler.

izinler1 / 31içeride2bitti3içeride4bekliyor5bekliyor

2 sn sonra 2 çıkar ve iznini bırakır.

izinler2 / 31bitti2bitti3içeride4bekliyor5bekliyor

1 de çıkar.

izinler0 / 31bitti2bitti3içeride4içeride5içeride

Bekleyen 4 ve 5 izinleri alıp girer.

izinler3 / 31bitti2bitti3bitti4bitti5bitti

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());
    }
}
Çıktı
Sonuç: 200000
true 0
false 0
  • AtomicInteger, AtomicLong, AtomicBoolean…: kilitsiz, güvenli erişim.
  • incrementAndGet() = atomik ++x; işlem yarıda kesilemez.
  • Atomik nesne ≈ volatile alan + atomik compareAndSet.
  • compareAndSet(beklenen, yeni): değer hâlâ beklenen ise yeni'yi yazar, true dö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");
    }
}
Çıktı
çalışıyor...
çalışıyor...
çalışıyor...
İş parçacığı iptal edildi!
main: bitti
  • interrupt(): kesme isteği; iş parçacığı sleep/wait'teyse InterruptedException.
  • 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();
Çıktı
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());
1. çalıştırma
thread1: Değer 1
thread2: Değer 2
main: null
2. çalıştırma
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ç set etmediği için null görür.
  • ThreadLocal.withInitial(() -> 0) (Java 8+) ya da initialValue()'yu geçersiz kılmak: her iş parçacığına ayrı başlangıç değeri.

Diğer Araçlar

İş Parçacığı Havuzları (Thread Pools)

submit(görev)görev kuyruğuG1G2G3G4iş parçacığı havuzu (2)pool-1-thread-1pool-1-thread-2iş parçacıkları biten görevden sonra yenisini alır
  • 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.
  • ExecutorService arayü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.

1. çalıştırma
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ı
2. çalıştırmada farklı olanlar
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.

Değişmez nesneler

Oluşturulduktan sonra değişmeyen nesneler (String, final alanlı sınıflar, Java 16+ record) senkronizasyonsuz paylaşılabilir.

Paylaşmamak

Yerel değişkenler her iş parçacığının yığınındadır; ThreadLocal da kopya verir. Paylaşılmayan veri yarışmaz.

Senkronizasyon

Paylaşılan değişen veriye her erişim aynı kilitle korunur: synchronized, Lock.

Hazır yapılar

AtomicInteger, ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue; ArrayList ve HashMap güvenli değildir.

Özet

Özet

KonuAna fikirAraç
İş 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 → TERMINATEDgetState()
Durdurmaİş birliğiyle: bayrak ya da kesmevolatile, interrupt()
Yarış durumuPaylaşılan veride bölünebilir işlemler; sonuç sıraya bağlısynchronized, Lock
GörünürlükBir iş parçacığının yazdığı diğerine görünmeyebilirvolatile
KilitlenmeKilitleri farklı sırada almak döngü yaratırSabit sıra, tryLock
Koşul beklemeKilidi bırakarak bekle, değişince uyandırwait/notify, Condition
KoordinasyonSayma, buluşma, takas, izinCountDownLatch, CyclicBarrier, Exchanger, Semaphore
DiğerKilitsiz sayaç, iş parçacığına özel değer, havuzAtomicInteger, ThreadLocal, ExecutorService

Özet

İlgili Sayfalar

Çoklu İş Parçacığı ve Paralellik Kavramları

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)

HataBelirtiDoğrusu
t.run() çağırmakYeni iş parçacığı yok; kod main'de sırayla çalışırt.start()
start()'ı iki kez çağırmakIllegalThreadStateExceptionHer çalıştırma için yeni Thread
Thread.sleep(100); yalnız başınaerror: unreported exception InterruptedException; must be caught or declared to be throwntry-catch ya da throws InterruptedException
Lambda içinde döngü değişkeni ierror: local variables referenced from a lambda expression must be final or effectively finalint 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 statementGövdede sleep/wait olmalı ya da catch kaldırılmalı
Sınıf Thread'i genişletmiyorken currentThread()error: cannot find symbolThread.currentThread()
join()'u unutmakmain, sonuç hazır olmadan okur (ör. 0)Sonucu okumadan önce join()

Pekiştirme

Sık Yapılan Hatalar (2)

HataBelirtiDoğrusu
wait()/notify()'ı synchronized dışında çağırmakIllegalMonitorStateException: current thread is not ownerAynı nesne üzerinde synchronized içinde
Koşulu if ile beklemekUyanınca koşul yine bozuk olabilir; taşan/boş kuyrukwhile (koşul) wait();
unlock()'u finally dışında yazmakİstisnada kilit hiç açılmaz; herkes sonsuza dek beklerlock(); try { … } finally { unlock(); }
Almadığı kilidi açmakIllegalMonitorStateExceptionHer lock() için bir unlock()
volatile sayaçta sayac++Artışlar kaybolur (ör. 163299)AtomicInteger ya da synchronized
final volatile alanerror: illegal combination of modifiers: final and volatileBirini seçin
stop(), suspend() kullanmakJava 20+: UnsupportedOperationExceptionBayrak, interrupt(), wait/notify
InterruptedException'ı boş catch ile yutmakKesme isteği kaybolur, iş parçacığı durmazDöngüden çık ya da Thread.currentThread().interrupt()
Kilitleri farklı sırada almakKilitlenme: program donarHerkes 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
  1. ABC, her zaman. A start'tan önce, B yeni iş parçacığında; join() B bitmeden C'ye geçirmez.
  2. NEW X NEW. run() sıradan çağrıdır: X'i main yazar, iş parçacığı hiç başlamaz, NEW kalır.
  3. 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
  1. a) TIMED_WAITING b) BLOCKED c) WAITING d) TERMINATED.
  2. Hayır: bakiye += m oku–ekle–yaz'dır. Metotları synchronized yapın ya da AtomicInteger.addAndGet(m).
  3. Kilitlenme: T1 a'yı, T2 b'yi alırsa ikisi de ötekini sonsuza dek bekler. Çözüm: ikisi de önce a, sonra b.

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 Callable görevleriyle hesaplayın ve Future'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).

Son

Bölüm 9: İş Parçacıkları
Sonraki bölüm: Bölüm 10: Veri Tabanı
1 / 1 Sercan KÜLCÜ, Tüm hakları saklıdır.