Java’da Multithreading – Bölüm 13: Threadlerin Yarıda Kesilmesi (Interrupting)

Bu zamana kadar ki örneklerimizde “InterruptedException” istisnası ile sıkça karşılaştık. Bu istisna bir threadin çalışması yarıda kesildiğinde fırlatılır. Peki çalışmakta olan bir threadin çalışması nasıl yarıda kesilir? Gelin yaptığı işlem uzun süren bir threadi belli süre geçtikten sonra durdurmayı deneyelim. Bunun için “Thread” sınıfının sağladığı “interrupt()” metodunu kullanacağız:

public class Application {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("Başladı.");

        Thread t = new Thread(() -> {
            for (int i = 0; i < 1E8; i++) {
                Math.sin(new Random().nextDouble());
            }
        });

        t.start();

        Thread.sleep(500);

        t.interrupt();

        t.join();

        System.out.println("Bitti.");
    }

}

Programın çalışması bittiğinde çıktısının şu şekilde olduğunu göreceksiniz: Okumaya devam et

Java’da Multithreading – Bölüm 12: “Callable” ve “Future”

Thread havuzlarını incelediğimiz bölümde görmüştük: “ExecutorService” sınıfının “submit” metoduna yapılacak işleri birer “Runnable” nesnesi şeklinde geçeriz, o da her bir “Runnable” nesnesi için bir thread oluşturur ve başlatır. “submit” metodu eklediğimiz her bir “Runnable” için bir “Future” döndürür. Bu “Future” nesneleri sayesinde asenkron işletilen bu görevlerin tamamlanıp tamamlanmadığını, yoksa iptal mi edildiklerini kontrol edebilir, hatta o görevleri iptal edebiliriz. Ayrıca yine bu “Future” nesnelerini kullanarak threadlerin dönüş değerlerini elde edebiliriz. Şaşırmış olmalısınız, çünkü “Runnable” arabiriminin “run()” metodu değer döndürmez, yani “void”dir. Değer döndürme ihtiyacımız olduğu durumlarda Java eş zamanlılık kütüphanesinde yer alan “Callable” adlı arabirimi kullanabilir, “submit” metoduna görevlerimizi “Callable” nesneleri olarak geçebiliriz. “Callable” arabirimi jenerik tiplidir ve sahip olduğu tek metot olan “call()” metodu “Callable” jeneriği için belirtilen tipte bir değer döner.

Aşağıda “submit” metoduna rastgele bir sayı dönen bir “Callable” geçtik. Çıktı olaraksa “Future” nesnesi üzerinden elde ettiğimiz bu rastgele sayıyı yazdıracak:

public class Application {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();

        Future future = executorService.submit(new Callable() {
            @Override
            public Integer call() throws Exception {
                Random random = new Random();
                return random.nextInt();
            }
        });

        executorService.shutdown();

        try {
            Integer result = future.get();
            System.out.println("Sonuç: " + result);
        } catch (ExecutionException e) {
            System.out.println("Çalışma zamanı hatası!");
        } catch (InterruptedException e) {
        }
    }

}

Okumaya devam et

Java’da Multithreading – Bölüm 11: Semaforlar

Semaforlar (Semaphores) -genelde- bir kaynağa erişimi kontrol etmek için kullanılan sayaçlardır. Kilit mekanizmaları gibi threadlerin senkronizasyonu için kullanılırlar fakat kilitlerden farklı olarak bağımsız threadler tarafından serbest bırakılabilirler (release).

HATIRLATMA! Bir thread tarafından elde edilmiş kilidi başka bir thread serbest bırakmaya kalktığında ”IllegalMonitorStateException” istisnası fırlatılır.

Örneğin programımızın bir sunucuya eş zamanlı isteklerde bulunması, ancak programımızdan o sunucuya eş zamanlı maksimum üç bağlantı olmasını istiyoruz. Tam da semaforlarla çözülebilecek bir durum. Kısıtlı kaynağımız “bağlantı sayısı”, üst sınır değeri ise “3”. Öyleyse 3 izinli (permit) bir semafor işimizi görecektir.

“java.util.concurrent.Semaphore” sınıfı kurucusu “permit” (izin) adlı bir parametre almaktadır. Bu değer ilgili kısıtın üst sınır değerini ifade eder. Örneğimiz için ifade edecek olursak, her bir thread bağlantı oluşturdukça semaforun “acquire()” (elde et) metodu ile semafordan bir izin alacaktır. İki thread daha bağlantı oluşturduğunda izin değeri “0” olacağından yeni bir thread izin istediğinde verilecek izin kalmadığından yeni threadler bekletilecektir. Ta ki önceki threadler işlerini bitirip -semaforun ”release()” metodunu kullanarak- aldıkları izni iade edene kadar.

Örnek programımız sunucu bağlantısı kurmaya çalışan 10 adet threadi oluşturup başlatan bir “main” metodu ve kontrollü bir şekilde sunucu bağlantılarını gerçekleştiren bir “Connection” sınıfından meydana gelmekte:

public class Application {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();

        for (int i = 0; i < 10; i++) {
            executorService.submit(() -> {
                try {
                    Connection.getInstance().connect();
                } catch (InterruptedException e) {
                }
            });
        }

        executorService.shutdown();
    }

}

“Connection” sınıfı: Okumaya devam et

Java’da Multithreading – Bölüm 10: Kilitlenme (Deadlock)

Bu bölümde çok threadli programların baş belası kilitlenme/tıkanma (deadlock) durumunu inceleyeceğiz.

Bu bölümdeki örnek programımızda iki banka hesabı arasındaki para transferini örnekleyeceğiz. Bir banka hesabını tanımlayan “Account” (Hesap) sınıfı aşağıdaki gibidir:

public class Account {

    private int balance = 10000;

    public static void transfer(Account sourceAccount, Account targetAccount, int amount) {
        sourceAccount.withdraw(amount);
        targetAccount.deposit(amount);
    }

    public void deposit(int amount) {
        balance += amount;
    }

    public void withdraw(int amount) {
        balance -= amount;
    }

    public int getBalance() {
        return balance;
    }

}

Okumaya devam et

Java’da Multithreading – Bölüm 9: Yeniden Girilir Kilitler (Re-entrant Locks)

Bu bölümde “synchronized” anahtar kelimesinin işlevine alternatif bir yöntem olan yeniden girilir kilitleri (re-entrant locks) inceleyeceğiz.

Yeniden girilir kilitlerin çalışma mantığı şu şekildedir: java.util.concurrent.locks.* paketi altındaki “ReentrantLock” sınıfı türünde yeniden girilir bir kilit oluşturulur. Aynı anda yalnızca bir threadin girmesini istediğimiz kod bloğu yeniden girilir kilidin “lock()” ve “unlock()” metotlarıyla çevrelenir.

Şimdi örnek programımız üzerinden incelememize devam edelim:

public class Runner {

    private int count = 0;

    private Lock lock = new ReentrantLock();

    private void increment() {
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }

    public void firstThread() {
        lock.lock();
        increment();
        lock.unlock();
    }

    public void secondThread() {
        lock.lock();
        increment();
        lock.unlock();
    }

    public void printCount() {
        System.out.println("Sayaç: " + count);
    }

}

Okumaya devam et

Java’da Multithreading – Bölüm 8: Beklet ve Bildir (Wait and Notify)

Bir önceki bölümde Producer-Consumer yapısını ve bir kuyruk ile nasıl gerçekleştirebileceğimizi incelemiştik. Kuyruğa eleman eklerken veya kuyruktan eleman alırken bazı koşullara göre ilgili threadlerin bekletildiğinden söz etmiştik. Bu bölümde bu işlevselliği kendi çok threadli programlarımız için nasıl gerçekleştirebileceğimizi öğreneceğiz.

Java’da her sınıf otomatik olarak “Object” sınıfından türer. “Object” üst sınıfına ait “wait()” ve “notify()” metotları yukarıda bahsettiğimiz bekleme ve tekrar sürdürme işlevselliğini sağlayabilmek için kullanılırlar. Bu bölümde “wait()” ve “notify()” metotlarının ne işe yaradıklarına ve nasıl kullanıldıklarına basit bir örnekle giriş yapmış olacağız.

Üretici ve tüketici metotlara yani “produce()” ve “consume()” metotlarına sahip Processor sınıfımız aşağıdaki gibi olsun:

public class Processor {

    public void produce() throws InterruptedException {
        synchronized (this) {
            System.out.println("Üretici thread çalışıyor...");
            wait();
            System.out.println("Üretici devam ediyor...");
        }
    }

    public void consume() throws InterruptedException {
        Thread.sleep(2000);

        Scanner scanner = new Scanner(System.in);

        synchronized (this) {
            System.out.println("Tüketici thread çalışıyor...");
            System.out.print("Devam etmek için 'Enter'a basınız: ");
            scanner.nextLine();
            System.out.println("Tüketici devam ediyor...");
            notify();
            Thread.sleep(5000);
            System.out.println("Tüketici 5 saniye daha devam etti.");
        }
    }

}

Okumaya devam et

Java’da Multithreading – Bölüm 7: Producer-Consumer Yapısı

Producer (Üretici) ve Consumer (Tüketici) yapısı hem günlük hayatta hem de programlama yaparken sıkça karşılaşabileceğimiz bir yapıdır. Producer kuyruğa bir şeyler ekler, Consumer ise bu kuyrukta bir şeyler oldukça sırayla alır ve ne yapması gerekiyorsa yapar. Günlük hayattan bir örnek verecek olursak; bir bankada müşteriler için sıra numarası üreten cihaza Producer, işlem görmemiş sıra numaralı müşteri oldukça onları LED sıra göstergeleri aracılığıyla çağırıp işlem yapan vezne görevlisi de Consumer olarak düşünülebilir.

Siz de fark etmişsinizdir, Producer ve Consumer birbirinden bağımsız iki ayrı thread halinde gerçekleştirilebilir. Ancak burada yine ortak bir veri kaynağımız olduğunu gözden kaçırmayalım: Kuyruk (Queue). Yine yardımımıza yetişen java.util.concurrent.* paketinden “thread-safe” bir yardımcı sınıf olan “BlockingQueue” oluyor. BlockingQueue aslında bir arabirim (interface) ve biz onun gerçekleştirimlerinden (implementation) biri olan “ArrayBlockingQueue” sınıfını kullanacağız. ArrayBlockingQueue tipinde bir kuyruk oluştururken bir kapasite belirtiriz. Bu kapasite dolu ise kuyruğa yeni eleman eklemek isteyen thread bekletilir. Benzer şekilde eğer kuyrukta eleman yoksa kuyruktan eleman almak isteyen thread kuyrukta eleman oluncaya kadar bekletilir.

Şimdi Producer-Consumer yapısını ve ArrayBlockingQueue sınıfının kullanımını örnekleyeceğimiz programımızı inceleyelim: Okumaya devam et