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;
    }

}

Hesapların başlangıç bakiyesini 10 bin olarak ayarladık. “deposit” metodu bir hesaba para yatırmak için kullanılırken “withdraw” metodu o hesaptan para çekmek için kullanılacak. “transfer” yardımcı metodu ise belirtilen miktarda parayı (“amount” parametresi ile belirtiliyor) kaynak hesaptan çekip hedef hesaba yatırmada kullanılacak.

Şimdi para transferini gerçekleştirecek “Runner” sınıfımızı inceleyelim:

public class Runner {

    private Account account1 = new Account();

    private Account account2 = new Account();

    public void firstThread() {
        Random random = new Random();

        for (int i = 0; i < 10000; i++) {
            Account.transfer(account1, account2, random.nextInt(100));
        }
    }

    public void secondThread() {
        Random random = new Random();

        for (int i = 0; i < 10000; i++) {
            Account.transfer(account2, account1, random.nextInt(100));
        }
    }

    public void printBalance() {
        System.out.println("Hesap 1'in bakiyesi: " + account1.getBalance());
        System.out.println("Hesap 2'in bakiyesi: " + account2.getBalance());
        System.out.println("Toplam bakiye: " + (account1.getBalance() + account2.getBalance()));
    }

}

“Runner” sınıfı, “main” metodunun eş zamanlı iyi ayrı thread içerisinde işleteceği iki metoda (“firstThread” ve “secondThread” metotları), bu iki thread sonlandığında “main” metodu tarafından çağrılarak hesap bakiyelerinin son durumlarını yazdıracak “printBalance()” metoduna ve son olarak iki adet hesap nesnesine (“Account” tipinde) sahip. “firstThread()” metodu döngü içerisinde belli miktar parayı (rastgele bir miktar) birinci hesaptan ikinci hesaba transfer ediyor. “secondThread()” metodu ise benzer para transferini ters yönde, yani ikinci hesaptan birinci hesaba olacak şekilde gerçekleştiriyor. Ve bakın çıktımız nasıl oluyor:

Hesap 1'in bakiyesi: 2905
Hesap 2'in bakiyesi: 14716
Toplam bakiye: 17621

Bazı durumlarda toplam bakiyeyi 20 bin olarak görebilirsiniz ancak çoğu durumda böyle alakasız sonuçlar görürsünüz. Bunun nedeni elbette ki eş zamanlı çalışan threadler. Çünkü iki thread de aynı anda birinci ve ikinci hesap bakiye değerleri (balance) üzerinde işlem yapmakta. Bu hatalı durumu çözmek için bir kilit mekanizması kullanmalı. Gelin geçen bölümde incelediğimiz yeniden girilir kilitlerden (re-entrant locks) kullanalım:

public class Runner {

    private Account account1 = new Account();
    private Account account2 = new Account();

    private Lock lock1 = new ReentrantLock();
    private Lock lock2 = new ReentrantLock();

    public void firstThread() {
        Random random = new Random();

        for (int i = 0; i < 10000; i++) {
            lock1.lock();
            lock2.lock();

            try {
                Account.transfer(account1, account2, random.nextInt(100));
            } finally {
                lock1.unlock();
                lock2.unlock();
            }
        }
    }

    public void secondThread() {
        Random random = new Random();

        for (int i = 0; i < 10000; i++) {
            lock1.lock();
            lock2.lock();

            try {
                Account.transfer(account2, account1, random.nextInt(100));
            } finally {
                lock1.unlock();
                lock2.unlock();
            }
        }
    }

    ...

}

Bir hesap üzerinde aynı anda yalnızca bir işlem yapılması gerektiği için hesap başına bir adet yeniden girilir kilit oluşturduk. Ayrıca bir transfer işlemi sırasında aynı anda iki hesap üzerinde birden işlem yapıldığı için (birinden çekilip diğerine yatırılması şeklinde) transfer öncesi ilgili hesaplar için olan kilitlerin ikisi de kilitlenmeli ve işlem sonrasında iki kilit de serbest bırakılmalı.

Kilit mekanizması sayesinde artık çıktıyı her zaman toplam bakiye 20 bin olacak şekilde göreceğiz:

Hesap 1'in bakiyesi: 3985
Hesap 2'in bakiyesi: 16015
Toplam bakiye: 20000

Sorun giderildi. Yalnızca iki hesap olduğu için birer kilit oluşturup doğru sıralama ile “lock()” çağrılarını yaparak kolayca çözebildik. Peki “lock()” çağrılarının sıralamasını yanlış yapsaydık? Tıpkı aşağıdaki gibi:

public void firstThread() {
    ...

    for (int i = 0; i < 10000; i++) {
        lock1.lock();
        lock2.lock();

        ...
}

public void secondThread() {
    ...

    for (int i = 0; i < 10000; i++) {
        lock2.lock();
        lock1.lock();

        ...
}

Böyle bir durumda birinci thread birinci kilidi, ikinci thread ikinci kilidi elde edecek, ve aynı anda iki kilit de elde edildiği için kilitlenme yani “deadlock” meydana gelecekti. Böyle bir durumu önlemek için kullanılabilecek yöntemlerden biri “lock()” yerine “tryLock()” metodunu kullanmaktır. “tryLock()” metodu kilidi elde etmesi halinde “true” döner. Ancak belli süre geçtikten sonra hala kilidi elde edememişse bir istisna fırlatacaktır. Böylece biz durumdan haberdar oluruz ve belli süre sonra tekrar deneme gibi aksiyonlarda bulunabiliriz. Tıpkı aşağıdaki örneğimizde olduğu gibi:

public class Runner {

    ...

    private Lock lock1 = new ReentrantLock();
    private Lock lock2 = new ReentrantLock();

    private void acquireLocks(Lock firstLock, Lock secondLock) {
        while (true) {
            boolean isFirstLockAcquired = false;
            boolean isSecondLockAcquired = false;

            try {
                isFirstLockAcquired = firstLock.tryLock();
                isSecondLockAcquired = secondLock.tryLock();
            } finally {
                if (isFirstLockAcquired && isSecondLockAcquired) {
                    return;
                }

                if (isFirstLockAcquired) {
                    firstLock.unlock();
                }

                if (isSecondLockAcquired) {
                    secondLock.unlock();
                }
            }

            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
            }
        }
    }

    public void firstThread() {
        ...

        for (int i = 0; i < 10000; i++) {
            acquireLocks(lock1, lock2);

            ...
        }
    }

    public void secondThread() {
        ...

        for (int i = 0; i < 10000; i++) {
            acquireLocks(lock1, lock2);

            ...
        }
    }

    ...

}

“firstThread()” ve “secondThread()” metotları içerisindeki “lock()” çağrılarını kaldırarak yerine “acquireLocks” metodu çağrısını koyduk. “acquireLocks” metodu “tryLock()” metodunun sağladığı tekrar deneme yeteneğini kullanarak aynı anda iki kilit birden elde edilinceye kadar kilitleri elde etmeyi denemeye devam eder. Eğer kilitlerden yalnızca biri elde edilmişse kilitlenmeye (deadlock) sebebiyet vermemek için o kilidi serbest bırakır.

Reklamlar

Bir Yorum Yazın

Aşağıya bilgilerinizi girin veya oturum açmak için bir simgeye tıklayın:

WordPress.com Logosu

WordPress.com hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap / Değiştir )

Twitter resmi

Twitter hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap / Değiştir )

Facebook fotoğrafı

Facebook hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap / Değiştir )

Google+ fotoğrafı

Google+ hesabınızı kullanarak yorum yapıyorsunuz. Çıkış  Yap / Değiştir )

Connecting to %s