Wątki a problem z synchronizacją – klasa Lock

Wątki a problem z synchronizacją – klasa Lock

W przypadku, gdy 2 różne wątki operują np. za pośrednictwem jakiejś metody na tej samej zmiennej, czy jakimś obiekcie, pojawia się problem z synchronizacją. Mogą powstawać niepoprawne obiekty, zmienne mogą nabyć nieoczekiwanych wartości itd.. Idealnie obrazuje to poniższy przykład.

Dwa różne wątki wykonują te same operacje, przelewając pieniądze z jednego konta, na drugie. Generalnie saldo obrazujące ogólną ilość pieniędzy nie powinno być różne od wartości 4000, która jest sumą pieniędzy z dwóch kont. W przykładzie na kontach może być mniej niż 0 złotówek.

  1. package threadstest;
  2. import java.util.Random;
  3. import java.util.concurrent.locks.Lock;
  4. import java.util.concurrent.locks.ReentrantLock;
  5. class Bank {
  6. private int konta[] = new int[2];
  7. private int saldo;
  8. private static int nrOperacji;
  9. public Bank() {
  10. konta[0] = 2000;
  11. konta[1] = 2000;
  12. saldo = konta[0] + konta[1];
  13. }
  14. public void przelej(int i, int j, int kwota) {
  15. nrOperacji++;
  16. konta[i] -= kwota;
  17. konta[j] += kwota;
  18. saldo = konta[i] + konta[j];
  19. System.out.println("Transfer z konta [" + i + "] (" + konta[i] +") na [" + j + "] (" + konta[j] + "). Przelana kwota: " + kwota + " zł. Saldo wynosi: "+ saldo + " zł. Operacja nr " + nrOperacji + ".");
  20. }
  21. }
  22. public class ThreadsTest {
  23. public static void main(String[] args) {
  24. Bank bank = new Bank();
  25. Random random = new Random();
  26. while(true) {
  27. Runnable run = new Runnable() {
  28. public void run() {
  29. int test = random.nextInt(2); // Losuje, z którego konta ma zostać wykonany przelew.
  30. if (test == 1) {
  31. bank.przelej(0, 1, random.nextInt(70));
  32. } else {
  33. bank.przelej(1, 0, random.nextInt(70));
  34. }
  35. }
  36. };
  37. Thread thread = new Thread(run);
  38. Thread thread2 = new Thread(run);
  39. thread.start();
  40. thread2.start();
  41. }
  42. }
  43. }
package threadstest;

import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
class Bank {
    private int konta[] = new int[2];
    private int saldo;
    private static int nrOperacji;
     
    public Bank() {
        konta[0] = 2000;
        konta[1] = 2000;
        saldo = konta[0] + konta[1];
    }
 
    public void przelej(int i, int j, int kwota) {
        nrOperacji++;
        konta[i] -= kwota;
        konta[j] += kwota;
        saldo = konta[i] + konta[j];
        System.out.println("Transfer z konta [" + i + "] (" + konta[i] +") na [" + j + "] (" + konta[j] + "). Przelana kwota: " + kwota + " zł. Saldo wynosi: "+ saldo + " zł. Operacja nr " + nrOperacji + ".");
    }
}
 
public class ThreadsTest {
 
    public static void main(String[] args) {
       Bank bank = new Bank();
       Random random = new Random();
        
       while(true) {
           Runnable run = new Runnable() {
               public void run() {
                   int test = random.nextInt(2);    // Losuje, z którego konta ma zostać wykonany przelew.
                   if (test == 1) {
                       bank.przelej(0, 1, random.nextInt(70));
                   } else {
                       bank.przelej(1, 0, random.nextInt(70));
                   }
               }
           };
            
           Thread thread = new Thread(run);
           Thread thread2 = new Thread(run);
            
           thread.start();
           thread2.start();
       }
    }
     
}

Jednak w tym przypadku dzieją się rzeczy, które miejsca mieć nie powinny. Z powodu „stylu” w jakim działają wątki (krótko ujmując chwilę działa jeden, chwilę drugi – itd. aż nie wykonają swoich zadań), saldo w tym konkretnym przypadku zmienia (teoretycznie) niezmienialną wartość 4000 zł. W BARDZO dużym skrócie dzieje się tak dlatego, że jeden z wątków zostaje zatrzymany w celu ustąpienia innemu, nie kończąc tym samym swojego zadania do końca.

Wystarczy nawet kilkadziesiąt „transferów / przelewów”, by saldo zmieniło wartość. W tym przypadku widać, że nawet zmienna nrOperacji się „sypie”:

Transfer z konta [1] (1815) na [0] (2185). Przelana kwota: 14 zł. Saldo wynosi: 4000 zł. Operacja nr 63.
Transfer z konta [1] (1809) na [0] (2191). Przelana kwota: 57 zł. Saldo wynosi: 4000 zł. Operacja nr 61.
Transfer z konta [1] (1621) na [0] (2416). Przelana kwota: 36 zł. Saldo wynosi: 4037 zł. Operacja nr 72.
Transfer z konta [0] (2380) na [1] (1657). Przelana kwota: 20 zł. Saldo wynosi: 4037 zł. Operacja nr 71.

Jedną z możliwości naprawienia tego problemu jest wykorzystanie obiektu klasy Lock i jego metod lock i unlock. Taka opcja pozwala na dostęp do metody tylko jednemu wątkowi w tym samym czasie. Wątek musi zakończyć pracę z metodą, by kolejny mógł ją rozpocząć:

  1. package threadstest;
  2. import java.util.Random;
  3. import java.util.concurrent.locks.Lock;
  4. import java.util.concurrent.locks.ReentrantLock;
  5. class Bank {
  6. private int konta[] = new int[2];
  7. private int saldo;
  8. private static int nrOperacji = 0;
  9. public Bank() {
  10. konta[0] = 2000;
  11. konta[1] = 2000;
  12. saldo = konta[0] + konta[1];
  13. }
  14. private Lock przelejLock = new ReentrantLock(); // odpowiada za nałożenie blokady
  15. public void przelej(int i, int j, int kwota) {
  16. przelejLock.lock(); // blokada włączona zaraz przed rozpoczęciem działania metody
  17. try {
  18. nrOperacji++;
  19. konta[i] -= kwota;
  20. konta[j] += kwota;
  21. saldo = konta[i] + konta[j];
  22. System.out.println("Transfer z konta [" + i + "] (" + konta[i] +") na [" + j + "] (" + konta[j] + "). Przelana kwota: " + kwota + " zł. Saldo wynosi: "+ saldo + " zł. Operacja nr " + nrOperacji + ".");
  23. } finally {
  24. przelejLock.unlock(); // blokada zdjęta po zakończeniu zadań z bloku try
  25. }
  26. }
  27. }
  28. public class ThreadsTest {
  29. public static void main(String[] args) {
  30. Bank bank = new Bank();
  31. Random random = new Random();
  32. while(true) {
  33. Runnable run = new Runnable() {
  34. public void run() {
  35. int test = random.nextInt(2); //losuje z którego konta ma zostać wykonany przelew
  36. if(test == 1) {
  37. bank.przelej(0, 1, random.nextInt(70));
  38. } else {
  39. bank.przelej(1, 0, random.nextInt(70));
  40. }
  41. }
  42. };
  43. Thread thread = new Thread(run);
  44. Thread thread2 = new Thread(run);
  45. thread.start();
  46. thread2.start();
  47. }
  48. }
  49. }
package threadstest;
 
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
class Bank {
    private int konta[] = new int[2];
    private int saldo;
    private static int nrOperacji = 0;
     
    public Bank() {
        konta[0] = 2000;
        konta[1] = 2000;
        saldo = konta[0] + konta[1];
    }
     
    private Lock przelejLock = new ReentrantLock();    // odpowiada za nałożenie blokady
     
    public void przelej(int i, int j, int kwota) {
        przelejLock.lock();    // blokada włączona zaraz przed rozpoczęciem działania metody
        try {
            nrOperacji++;
            konta[i] -= kwota;
            konta[j] += kwota;
            saldo = konta[i] + konta[j];
            System.out.println("Transfer z konta [" + i + "] (" + konta[i] +") na [" + j + "] (" + konta[j] + "). Przelana kwota: " + kwota + " zł. Saldo wynosi: "+ saldo + " zł. Operacja nr " + nrOperacji + ".");
        } finally {
            przelejLock.unlock();    // blokada zdjęta po zakończeniu zadań z bloku try
        }
    }
}
 
public class ThreadsTest {
 
    public static void main(String[] args) {
       Bank bank = new Bank();
       Random random = new Random();
        
       while(true) {
           Runnable run = new Runnable() {
               public void run() {
                   int test = random.nextInt(2);    //losuje z którego konta ma zostać wykonany przelew
                   if(test == 1) {
                       bank.przelej(0, 1, random.nextInt(70));
                   } else {
                       bank.przelej(1, 0, random.nextInt(70));
                   }
               }
           };
            
           Thread thread = new Thread(run);
           Thread thread2 = new Thread(run);
            
           thread.start();
           thread2.start();
       }
    }
     
}


Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *