이전 프로세스와 스레드를 코드로 정리했다.
이번엔 스레드 동기화에대해 알아보자.
늘 사용하던 단어이지만 의미를 한번 더 짚고 넘어가려고 한다.
동기화를 배우면서 '스레드'를 조금 더 이해할 수 있었기에 짚고 넘어간다.
위키백과에서 동기화는 시스템을 동시에 작동시키기 위해 여러 사건들을 조화시키는 것을 의미한다고 설명하고 있다.
네이버 어학사전은 작업들 사이의 수행 시기를 맞추는 것, 사건이 동시에 일어나거나, 일정한 간격을 두고 일어나도록 시간의 간격을 조정하는 것이라 설명한다.
👻약간의 제약(공유 정보는 한 스레드만 사용할 수 있도록)을 통해 공유하는 스레드가 최신 정보를 실시간으로 공유할 수 있도록 하는 것이라고 개인적으로 정리를 해보고 글을 시작하겠다.
(+ java 코드를 보고나면 더 잘 이해할 수 있다.)
싱글 스레드 프로그램에서는 1개의 스레드가 객체를 독차지해서 사용하면 되지만, 멀티 스레드 프로그램에서는 스래드끼리 객체를 공유해서 작업해야 하는 경우가 있다.
이 경우에 문제가 발생할 가능성이 있다.
다수의 스레드가 동시에 공유 데이터에 접근하면 공유 데이터가 훼손되는 문제가 발생한다.
두 스레드가 동시에 공유 데이터를 읽는 경우,
문제가 발생하지 않는다.
A 스레드는 쓰고 B 스레드는 읽을 경우,
: 읽고 쓰는 순서에 따라 읽는 값이 달라질 가능성이 있지만 공유 데이터의 훼손은 없다.
A 스레드와 B 스레드가 동시에 공유 데이터에 쓰는 경우,
데이터의 훼손 가능성이 있다.
✨ 이런 경우 스레드 동기화를 사용하면 A스레드가 해당 객체를 사용할때 B 스레드가 대기 상태로 둘 수 있다. (FIFO의 느낌)
위와 같이 여러 스레드가 공유 변수에 접근할 때, 공유 데이터 훼손을 막기 위해서는 A 스레드가 공유 데이터 사용을 마칠때까지 다른 스레드가 공유 데이터에 접근 못하도록 제어(lock)하는 키워드이다.
즉, 동기화를 가능하게 하는 키워드
(운영체제도 정리해야하는데, 이번 글은 코드, 키워드에 집중하도록 하겠다.)
임계구역(critical Section)
공유 데이터에 접근하는 프로그램 코드들을 의미한다.
상호배제(Mutual Exelusion)
임계구역을 오직 한 스레드만 배타적, 독점적으로 사용하는 기술을 의미한다.
- 임계구역에 먼저 진입한 스레드의 실행이 끝날 때까지 다른 스레드가 진입하지 못하도록 보장한다.
전에는 스레드를 2개 만들어 구현해봤다.
그동안은 읽고 출력하는 코드였기에 문제가 크게 없었다.
객체를 공유하여 사용할때 문제를 코드로 이해해보자.
✒️상황
(현실에서는 이러면 안되지만 가정이다.)
tom이 계좌를 계설해 통장이 있는데 lily도 해당 통장을 사용할 수도 있다.
통장에는 10000원이 있었다. 즉, 이 10000원이 공유자원이다.
tom은 입금을 하고 wife인 lily는 출금을 한다고 가정해보자.
public class Bank{
private int money = 10000; //공유자원으로 사용할 자료
public int getMoney(){ // getter
return money;
}
public void setMoney(int money){ //setter
this.money = money;
}
public void saveMoney(int mon){ //입금 기능
int m = this.getMoney();
try{
Thread.sleep(2000); // 입금 시 지연시간(2초) 표현
} catch (Exception e) {
System.out.println(e);
}
setMoney(m + mon);
}
public void minusMoney(int mon){ //출금 기능
int m = this.getMoney();
if (mon > m) {
System.out.println("잔고액 보다 출금액이 더 큽니다.");
return; //메소드의 탈출
}
try {
Thread.sleep(3000); // 은행에 출금 시 약간의 지연 시간(3초)을 표현
} catch (Exception e) {
System.out.println(e);
}
setMoney(m - mon);
}
}
실행되어야하는 메서드가 있어야한다. main 메서드를 만들기 위해 main클래스를 만들도록 하자. 위의 bank클래스로 접근하기 위해서는 객체 생성을 해야한다.
바로 불러올 수 있도록 static으로 만들것이다.
public class BankMain {
public static Bank bank = new Bank();
//생성자를 호출해서 객체 변수에 치환했다.
//static 객체 변수로 만든 이유는 객체 변수명으로 바로 접근하기 위해서이다.
public static void main (String[] args){
System.out.println("원금 : " + bank.getMoney() );
Tom tom = new Tom(); // tom 인스턴스를 만들었다.
Lily lily = new Lily(); // lily 인스턴스를 만들었다.
tom.start(); // 스레드를 실행시킨다.
lily.start(); // 스레드를 실행시킨다.
}
}
tom의 클래스를 만들것이다. 이용객을 하나의 흐름, 즉 스레드로 만들것이다.
implement로 runnable해도 되지만 Thread 객체를 만들어야하는 과정을 생략하고자
Thread를 상속받기로하자.
public class Tom extends Thread {
@Override
public void run(){
BankMain.bank.saveMoney(5000); // tom이 입금
//main을 통해서 Bank에 접근했다.
System.out.println("남편 Tom 예금 후 잔고: "+BankMain.bank.getMoney());
//Bank클래스에서 money를 가져온다.
}
}
lily의 클래스를 만들것이다. lily가 출금하는 상황이다.
tom과 동일하지만 implement로 runnable해도 되지만 Thread 객체를 만들어야하는 과정을 생략하고자 Thread를 상속받기로하자.
public class Lily extends Thread {
@Override
public void run(){
BankMain.bank.minusMoney(2000); // Lily의 출금
//main을 통해서 Bank클래스에 접근했다.
System.out.println("아내 Lily 출금 후 잔고: "+BankMain.bank.getMoney());
//Bank클래스에서 money를 가져온다.
}
}
이 코드를 통해 우리가 원하는 결과가 무엇인가?
메서드 실행이 누가 먼저 끝날지 확신할 수 없지만, (무슨 실행이 먼저이든) 원금이 10,000원이 있었고 톰이 5,000원을 입금했고 릴리가 2,000원을 출금했으니 결과는 13,000원이길 바란다.
결과를 확인해보자.
원금 : 10000
남편 Tom 예금 후 잔고: 15000
Lily 출금 후 잔고: 8000
결과는 우리가 원하던 방식대로 몇초뒤에 출금과 적금이 이뤄졌다.
결과를 확인해보자. 우리가 원하는 결과인가? 아니다.
tom과 lily는 원금을 각자 읽어와서 각자의 과정을 수행했다.
시간 또한 의도한대로 입금은 2초가 걸렸고 출금은 3초가 각각 걸렸다.
위와 같이 특정 데이터를 동기화(바뀌면 바뀐 정보를 사용하도록)하고 싶다면, synchronized 키워드를 사용하면 된다.
Bank 클래스에 공유자원을 사용하는 메서드에 키워드를 추가해주면된다.
public class Bank{
private int money = 10000; //공유자원으로 사용할 자료
public int getMoney(){ // getter
return money;
}
public void setMoney(int money){ //setter
this.money = money;
}
public synchronized void saveMoney(int mon){ //입금 기능
int m = this.getMoney();
try{
Thread.sleep(2000) ;// 입금 시 지연시간(2초) 표현
} catch (Exception e) {
System.out.println(e);
}
setMoney(m + mon);
}
public synchronized void minusMoney(int mon){ //출금 기능
int m = this.getMoney();
if (mon > m) {
System.out.println("잔고액 보다 출금액이 더 큽니다.");
return; //메소드의 탈출
}
try {
Thread.sleep(3000); // 은행에 출금 시 약간의 지연 시간(3초)을 표현
} catch (Exception e) {
System.out.println(e);
}
setMoney(m - mon);
}
}
결과를 확인해보자.
원금 : 10000
남편 Tom 예금 후 잔고: 15000
Lily 출금 후 잔고: 13000
원하는 결과가 나왔다!
synchronized 키워드로 인해 걸리는 시간 또한 다르다. 이전 실행결과는 각자 시간이 걸렸다면 동기화를 했기때문에 출금결과가 나오기까지는 5초가 걸렸다.
A 스레드가 공유 자원을 사용하는 경우, B 스레드는 바로 접근하지 못한다.(lock기능) A 스레드가 공유 자원을 사용을 마칠때까지 대기 상태가 된다.
배운 용어들로 멋있게 풀어보자면, 하나의 스레드가 임계구역에 들어오면 상호배제 조건을 만족시키기 위해 lock을 걸어 접근을 제한시킨다.
이를 통해 공유하는 데이터에 최신성을 유지한다.