Ders İçeriği

Java, çoklu iş parçacığı (multithreading) desteği sayesinde aynı anda birden fazla görevi eş zamanlı olarak yürütebilen uygulamalar geliştirmemizi sağlar. Bu, özellikle performans gerektiren uygulamalar, kullanıcı arayüzleri ve sunucu tarafı programlama için kritik öneme sahiptir. Bu bölümde, Java'da iş parçacığı oluşturma, yönetme ve senkronizasyon konularını inceleyeceğiz.

İş Parçacığı (Thread) Nedir?

İş parçacığı, bir programın (prosesin) en küçük yürütme birimidir. Her program en az bir iş parçacığına (ana iş parçacığı) sahiptir. Çoklu iş parçacığı, bir programın aynı anda birden fazla iş parçacığını çalıştırması anlamına gelir. Bu, CPU'nun boşta kalmasını önleyerek uygulamanın daha verimli çalışmasını sağlar.

Java'da İş Parçacığı Oluşturma

Java'da iş parçacığı oluşturmanın iki temel yolu vardır:

1. Thread Sınıfını Genişleterek

java.lang.Thread sınıfını genişleterek kendi iş parçacığı sınıfımızı oluşturabiliriz. Bu sınıfın run() metodunu geçersiz kılmamız (override) gerekir. run() metodu, iş parçacığının yürüteceği kodu içerir.

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
            try {
                Thread.sleep(500); // 500 milisaniye bekle
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + " kesintiye uğradı.");
            }
        }
    }
}

public class ThreadOrnek1 {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        thread1.setName("Thread-1");
        MyThread thread2 = new MyThread();
        thread2.setName("Thread-2");

        thread1.start(); // İş parçacığını başlat
        thread2.start(); // İş parçacığını başlat
    }
}
  • start() metodu, iş parçacığını başlatır ve JVM'in run() metodunu ayrı bir iş parçacığında yürütmesini sağlar. Doğrudan run() metodunu çağırmak, kodu ana iş parçacığında yürütür ve çoklu iş parçacığı sağlamaz.

2. Runnable Arayüzünü Uygulayarak

java.lang.Runnable arayüzünü uygulamak, iş parçacığı oluşturmanın daha esnek bir yoludur. Bu yöntem, sınıfınızın başka bir sınıftan miras almasını engellemez (Java'da çoklu kalıtım yoktur).

class MyRunnable implements Runnable {
    private String threadName;

    public MyRunnable(String name) {
        this.threadName = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(threadName + ": " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                System.out.println(threadName + " kesintiye uğradı.");
            }
        }
    }
}

public class ThreadOrnek2 {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyRunnable("Runnable-1"));
        Thread thread2 = new Thread(new MyRunnable("Runnable-2"));

        thread1.start();
        thread2.start();
    }
}

İş Parçacığı Yaşam Döngüsü

Bir iş parçacığı, yaşam döngüsü boyunca farklı durumlar arasında geçiş yapar:

  1. New (Yeni): İş parçacığı nesnesi oluşturulduğunda bu durumdadır (new Thread()).
  2. Runnable (Çalıştırılabilir): start() metodu çağrıldığında iş parçacığı bu duruma geçer. İşletim sistemi zamanlayıcısı tarafından çalıştırılmayı bekler.
  3. Running (Çalışıyor): İş parçacığı zamanlayıcı tarafından seçildiğinde ve CPU üzerinde yürütüldüğünde bu durumdadır.
  4. Blocked/Waiting (Engellenmiş/Bekliyor): İş parçacığı bir kaynağı beklerken (örneğin, I/O işlemi, kilit) veya sleep()wait()join() gibi metotlar çağrıldığında bu duruma geçer.
  5. Terminated (Sonlanmış): run() metodu tamamlandığında veya bir istisna fırlatıldığında iş parçacığı sonlanır.

İş Parçacığı Senkronizasyonu

Birden fazla iş parçacığı aynı paylaşılan kaynağa (değişken, dosya, veritabanı bağlantısı vb.) aynı anda erişmeye çalıştığında veri tutarsızlıkları meydana gelebilir. Bu durumu önlemek için iş parçacığı senkronizasyonu kullanılır.

synchronized Anahtar Kelimesi

synchronized anahtar kelimesi, bir metodu veya kod bloğunu aynı anda yalnızca bir iş parçacığının erişebileceği şekilde kilitlemek için kullanılır. Bu, kritik bölümlere eş zamanlı erişimi engeller.

  • synchronized Metot: Bir metot synchronized olarak işaretlendiğinde, o metodun ait olduğu nesnenin kilidi alınır. Aynı anda sadece bir iş parçacığı bu metodu yürütebilir.

    class Sayac {
        private int count = 0;
    
        public synchronized void arttir() {
            count++;
            System.out.println(Thread.currentThread().getName() + ": " + count);
        }
    
        public int getCount() {
            return count;
        }
    }
    
    class SayacRunnable implements Runnable {
        private Sayac sayac;
    
        public SayacRunnable(Sayac sayac) {
            this.sayac = sayac;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                sayac.arttir();
            }
        }
    }
    
    public class SenkronizasyonOrnek {
        public static void main(String[] args) throws InterruptedException {
            Sayac sayac = new Sayac();
            Thread t1 = new Thread(new SayacRunnable(sayac), "Thread-A");
            Thread t2 = new Thread(new SayacRunnable(sayac), "Thread-B");
    
            t1.start();
            t2.start();
    
            t1.join(); // t1'in bitmesini bekle
            t2.join(); // t2'nin bitmesini bekle
    
            System.out.println("Son Sayac Değeri: " + sayac.getCount()); // Beklenen: 2000
        }
    }
    
  • synchronized Blok: Daha ince taneli kontrol sağlamak için kullanılır. Belirli bir nesne üzerinde kilitlenir.

    class SayacBlok {
        private int count = 0;
        private Object lock = new Object(); // Kilit nesnesi
    
        public void arttir() {
            synchronized (lock) { // lock nesnesi üzerinde kilitlen
                count++;
                System.out.println(Thread.currentThread().getName() + ": " + count);
            }
        }
    
        public int getCount() {
            return count;
        }
    }
    // Kullanımı Sayac sınıfı ile aynıdır.
    

wait()notify()notifyAll() Metotları

Bu metotlar, iş parçacıklarının belirli koşullar altında birbirleriyle iletişim kurmasını ve senkronize olmasını sağlar. Bu metotlar Object sınıfının metotlarıdır ve yalnızca synchronized blok veya metot içinde çağrılabilirler.

  • wait(): İş parçacığını beklemeye alır ve nesnenin kilidini serbest bırakır.
  • notify(): Bekleyen iş parçacıklarından birini uyandırır.
  • notifyAll(): Bekleyen tüm iş parçacıklarını uyandırır.

join() Metodu

join() metodu, bir iş parçacığının tamamlanmasını beklemek için kullanılır. Çağıran iş parçacığı, join() çağrısı yapılan iş parçacığı tamamlanana kadar bekler.

public class JoinOrnek {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("Çalışan İş Parçacığı: " + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t.start();
        System.out.println("Ana iş parçacığı bekliyor...");
        t.join(); // t iş parçacığının bitmesini bekle
        System.out.println("Ana iş parçacığı devam ediyor.");
    }
}

ExecutorService ve Future

Java 5 ile birlikte gelen java.util.concurrent paketi, iş parçacığı yönetimini kolaylaştıran daha yüksek seviyeli API'ler sunar. ExecutorService, iş parçacığı havuzlarını yönetmek için kullanılırken, Future asenkron işlemlerin sonuçlarını temsil eder.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Callable;

public class ExecutorServiceOrnek {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(2); // 2 iş parçacıklı havuz

        // Runnable görevi
        executor.submit(() -> {
            System.out.println("Runnable görevi çalışıyor.");
        });

        // Callable görevi (değer döndürür)
        Future<Integer> future = executor.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("Callable görevi çalışıyor.");
                Thread.sleep(1000);
                return 123;
            }
        });

        System.out.println("Görevin sonucu: " + future.get()); // Sonucu alana kadar bekle

        executor.shutdown(); // Havuzu kapat
    }
}

Çoklu iş parçacığı, modern Java uygulamaları için vazgeçilmez bir özelliktir. Ancak, senkronizasyon ve iş parçacığı güvenliği konularına dikkat etmek, karmaşık hataların önüne geçmek için hayati öneme sahiptir. Bir sonraki derste Veritabanı Bağlantısı (JDBC) konusunu inceleyeceğiz.