스레드 동기화 java 코드로 보기 (Synchronized키워드, wait()와 notify())

하히호호·2024년 4월 21일
0

이전 프로세스와 스레드를 코드로 정리했다.
이번엔 스레드 동기화에대해 알아보자.

🤔동기화

늘 사용하던 단어이지만 의미를 한번 더 짚고 넘어가려고 한다.
동기화를 배우면서 '스레드'를 조금 더 이해할 수 있었기에 짚고 넘어간다.

위키백과에서 동기화는 시스템을 동시에 작동시키기 위해 여러 사건들을 조화시키는 것을 의미한다고 설명하고 있다.

네이버 어학사전은 작업들 사이의 수행 시기를 맞추는 것, 사건이 동시에 일어나거나, 일정한 간격을 두고 일어나도록 시간의 간격을 조정하는 것이라 설명한다.

👻약간의 제약(공유 정보는 한 스레드만 사용할 수 있도록)을 통해 공유하는 스레드가 최신 정보를 실시간으로 공유할 수 있도록 하는 것이라고 개인적으로 정리를 해보고 글을 시작하겠다.
(+ java 코드를 보고나면 더 잘 이해할 수 있다.)

스레드 동기화

싱글 스레드 프로그램에서는 1개의 스레드가 객체를 독차지해서 사용하면 되지만, 멀티 스레드 프로그램에서는 스래드끼리 객체를 공유해서 작업해야 하는 경우가 있다.
이 경우에 문제가 발생할 가능성이 있다.

스레드 동기화시 주의 사항

다수의 스레드가 동시에 공유 데이터에 접근하면 공유 데이터가 훼손되는 문제가 발생한다.

공유 데이터 훼손되는 문제 발생 가능

  • 두 스레드가 동시에 공유 데이터를 읽는 경우,
    문제가 발생하지 않는다.

  • A 스레드는 쓰고 B 스레드는 읽을 경우,
    : 읽고 쓰는 순서에 따라 읽는 값이 달라질 가능성이 있지만 공유 데이터의 훼손은 없다.

  • A 스레드와 B 스레드가 동시에 공유 데이터에 쓰는 경우,
    데이터의 훼손 가능성이 있다.

    ✨ 이런 경우 스레드 동기화를 사용하면 A스레드가 해당 객체를 사용할때 B 스레드가 대기 상태로 둘 수 있다. (FIFO의 느낌)

Synchronized 키워드

위와 같이 여러 스레드가 공유 변수에 접근할 때, 공유 데이터 훼손을 막기 위해서는 A 스레드가 공유 데이터 사용을 마칠때까지 다른 스레드가 공유 데이터에 접근 못하도록 제어(lock)하는 키워드이다.
즉, 동기화를 가능하게 하는 키워드

(운영체제도 정리해야하는데, 이번 글은 코드, 키워드에 집중하도록 하겠다.)

  • 사용자의 멀티스레드 프로그램에서 자주 발생한다. 커널에 공유 데이터가 많기 때문에 자주 발생한다.
    ++ 다중 코어에서 더욱 조심해야한다.

✏️ 임계구역과 상호배제

  • 임계구역(critical Section)
    공유 데이터에 접근하는 프로그램 코드들을 의미한다.

  • 상호배제(Mutual Exelusion)
    임계구역을 오직 한 스레드만 배타적, 독점적으로 사용하는 기술을 의미한다.
    - 임계구역에 먼저 진입한 스레드의 실행이 끝날 때까지 다른 스레드가 진입하지 못하도록 보장한다.

💻 java 코드로 이해하기

전에는 스레드를 2개 만들어 구현해봤다.
그동안은 읽고 출력하는 코드였기에 문제가 크게 없었다.

객체를 공유하여 사용할때 문제를 코드로 이해해보자.


✒️상황
(현실에서는 이러면 안되지만 가정이다.)
tom이 계좌를 계설해 통장이 있는데 lily도 해당 통장을 사용할 수도 있다.
통장에는 10000원이 있었다. 즉, 이 10000원이 공유자원이다.
tom은 입금을 하고 wife인 lily는 출금을 한다고 가정해보자.


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 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 메서드를 만들기 위해 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 클래스(하나의 스레드)

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의 클래스를 만들것이다. 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 사용 후

위와 같이 특정 데이터를 동기화(바뀌면 바뀐 정보를 사용하도록)하고 싶다면, synchronized 키워드를 사용하면 된다.
Bank 클래스에 공유자원을 사용하는 메서드에 키워드를 추가해주면된다.

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을 걸어 접근을 제한시킨다.
이를 통해 공유하는 데이터에 최신성을 유지한다.

profile
읽히는 코드를 짜고싶습니다.

0개의 댓글