동기화 / 데드락

Jan 29, 2024
동기화 / 데드락

1. 동기화(Synchronization)와 데드락이란?

여러 스레드가 공유된 자원에 동시에 접근하여 예기치 않은 결과가 발생할 수도 있는데, 이걸 바로 교착상태(데드락)이라고 한다. 이러한 데드락 문제를 해결하기 위한게 '동기화(Synchronization)'다. 먼저 동기화된 메소드는 동시 호출되더라도 단계들이 겹치지 않는다. 하나의 스레드가 동기화된 메소드를 실행하고 있으면, 그 스레드가 종료할 때까지 다른 모든 스레드는 중지된다. ex) 티켓팅에서 좌석이 1개 남았는데, 여러 스레드가 동시에 접근하는 바람에 2사람이 중복으로 예약되는 경우
notion image
화장실에 할당된 공간은 [2] 인데, add.a, add.b, add.c 를 동시에 하면.. 화장실이 5.....? 운영 불가능! 프로그램이 뻗어버림 -> 교착 상태 (데드락) * 교착 상태는 해결 방법이 전혀 없다. 재부팅밖에 답이 없으며 처음부터 일어나지 않게 해야한다.
💡
동시접근이 있으면 데드락이 있을 수도 있음. 무조건 발생하는거 아님!! 운 나쁘면 발생 ㅠㅠ
 

[데드락 발생 조건]

notion image
💡
자원 언제줘!!! 하면서 기다리다가 터지는거
 

2. 동기화 하지 않았을 경우 발생하는 문제 - 코드가 이해안됨

package ex16; class Printer { void print(int[] arr) { for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } class MyThread1 extends Thread { Printer prn; //prn이라는 이름의 Printer 객체를 사용하기 위한 준비를 하는 역할을 합니다. 이후에 생성자 등을 통해 이 변수에 실제 Printer 객체를 할당하고 사용할 수 있습니다. int[] myarr = { 10, 20, 30, 40, 50 }; MyThread1(Printer prn) { this.prn = prn; } public void run() { prn.print(myarr); } } class MyThread2 extends Thread { Printer prn; int[] myarr = { 1, 2, 3, 4, 5}; MyThread2(Printer prn) { this.prn = prn; } public void run() { prn.print(myarr); } } public class TestSynchro { public static void main(String[] args) { Printer obj = new Printer(); MyThread1 t1 = new MyThread1(obj); MyThread2 t2 = new MyThread2(obj); t1.start(); t2.start(); } }
notion image
notion image
하나의 Printer 객체를 여러 개의 스레드가 동시에 접근해서 출력하자, 출력할 때마다 값이 바뀜.
notion image
 

 
notion image
notion image
Printer 클래스를 필드에 쓰려고 생성자에 obj를 넣은듯?
TestSynchro 클래스에서 Printer 객체를 생성한 후, 이를 MyThread1과 MyThread2 객체의 생성자에 전달하여 prn 필드에 할당하는 것입니다. 이렇게 함으로써 MyThread1과 MyThread2는 동일한 Printer 객체를 사용하여 출력 작업을 수행합니다.
 

 
notion image
 

3. 데드락 예시

package ex16; public class DeadLockTest { public static void main(String[] args) { final String res1 = "Gold"; final String res2 = "Silver"; Thread t1 = new Thread(() -> { synchronized (res1) { System.out.println("Thread 1 : 자원 1 획득"); try { Thread.sleep(100); } catch (InterruptedException e) {} synchronized (res2) { System.out.println("Thread 1 : 자원 2 획득"); } } }); Thread t2 = new Thread(() -> { synchronized (res2) { System.out.println("Thread 2 : 자원 2 획득"); try { Thread.sleep(100); } catch (InterruptedException e) {} synchronized (res1) { System.out.println("Thread 2 : 자원 1 획득"); } } }); t1.start(); t2.start(); } }
notion image
 

[해결 방법]

간단하게만 알아보자. (자세히 알아보고 싶으면 찾아보기) Thread2가 Thread1과 동일한 순서대로 자원을 획득하면 교착 상태에 빠지지 않는다.
package ex16; public class DeadLockTest { public static void main(String[] args) { final String res1 = "Gold"; final String res2 = "Silver"; Thread t1 = new Thread(() -> { synchronized (res1) { System.out.println("Thread 1 : 자원 1 획득"); try { Thread.sleep(100); } catch (InterruptedException e) {} synchronized (res2) { System.out.println("Thread 1 : 자원 2 획득"); } } }); Thread t2 = new Thread(() -> { synchronized (res1) { System.out.println("Thread 2 : 자원 1 획득"); try { Thread.sleep(100); } catch (InterruptedException e) {} synchronized (res2) { System.out.println("Thread 2 : 자원 2 획득"); } } }); t1.start(); t2.start(); } }
 

4. 동기화 하는 법 (데드락 해결법)

공유된 자원 중, 동시에 사용하면 안 되는 자원을 보호하는 도구로 'synchronized' 를 메소드 앞에 입력한다. 그러면 여러 개의 스레드가 동시에 같은 작업을 수행할 때, synchronized를 이용하여 스레드들이 순서대로 작업을 하도록 만들 수 있다. 하나의 스레드가 자원을 사용하고 있을 때, 다른 스레드는 기다리는 것. 이렇게 하면 한 명씩 차례대로 자원을 사용할 수 있게 되고, 문제가 발생하지 않다. 이게 동기화!! 동시 접근을 애초에 막아버린다!!
notion image
 

[또다른 해결법]… 이긴 한데…

라운드 로빈(시분할) 알고리즘 사용. n초씩 분할해서 하나씩 들어가게 만드는 건데... 화장실을 예로 들었을 때, a, b, c가 각각 1초씩 들어갔다가 나오는걸 반복하면 굉장히.... 더럽고... 큰일이 나죠?
 

5. 예시

4-1. 동기적 접근 예시

package ex13.example1; import ex07.A; class Account { private int balance = 1000; public void withdraw(int amount) { if(amount <= balance) { balance = balance - amount; System.out.println("출금완료 : " + amount); } else { System.out.println("잔액이 부족합니다 : " + balance); } } } public class DeadEx01 { public static void main(String[] args) { Account account = new Account(); account.withdraw(1000); account.withdraw(1000); } }
notion image
notion image
 

4-2. 스레드 동시에 실행

package ex13.example1; import ex07.A; class Account { private int balance = 1000; public void withdraw(int amount) { if(amount <= balance) { System.out.println("출금완료 : " + amount); balance = balance - amount; } else { System.out.println("잔액이 부족합니다 : " + balance); } } } public class DeadEx01 { public static void main(String[] args) { Account account = new Account(); new Thread(() -> { account.withdraw(1000); }).start(); new Thread(() -> { account.withdraw(1000); }).start(); } }
notion image
동시에 실행되니까 잔액체크를 못 함. 그래서 출금을 두 번 해버림 ㅠㅠ
notion image
 

4-3. Synchronization 사용!!

package ex13.example1; import ex07.A; class Account { private int balance = 1000; synchronized public void withdraw(int amount) { if(amount <= balance) { System.out.println("출금완료 : " + amount); balance = balance - amount; } else { System.out.println("잔액이 부족합니다 : " + balance); } } } public class DeadEx01 { public static void main(String[] args) { Account account = new Account(); new Thread(() -> { account.withdraw(1000); }).start(); new Thread(() -> { account.withdraw(1000); }).start(); } }
notion image
notion image
 

6. Vector와 ArrayList의 동기화

Vector는 동기화를 지원하는 컬렉션 - 동시 접근 가능 (synchronized 지원) 멀티 스레드일 경우, 벡터를 쓰는게 안정적. 단일 스레드일 경우, 굳이 벡트 쓸 필요가 없으니 ArrayList 쓰자!
ArrayList는 동기화를 지원하지 않는 비동기 컬렉션 ArrayList를 여러 스레드에서 사용할 때에는 동기화 메커니즘을 추가로 구현하여 동시 접근을 제어해야 한다. 무슨말인지...
 
💡
read만 있을 때는 동기화 중요 x
write(변경이 있을 때)가 동기화 굉장히 중요 ★
Share article

codingb