[JAVA] 쓰레드 ( Thread ) ⑤

DongGyu Jung·2022년 3월 29일
0

자바(JAVA)

목록 보기
48/60
post-thumbnail

🏃‍♂️ 들어가기 앞서..

본 게시물은 스터디 활동 중에 작성한 게시물로 자바의 정석-기초편 교재를 학습하여 정리하는 글입니다.
※ 스터디 Page : 〔투 비 마스터 : 자바〕

*해당 교재의 목차 순서와 구성을 참고하여 작성하며
각 내용마다 부족할 수 있는 내용이나 개인적으로 궁금한 점은
추가적인 검색을 통해 채워나갈 예정입니다.



다시 한 번 간다히 되짚어보자면
" 싱글 쓰레드 프로세스 " 는 단 하나의 쓰레드만으로 작업하는 프로세스
" 멀티 쓰레드 프로세스 " 는 여러 개의 쓰레드로 여러가지의 작업을 수행하는 프로세스라고 볼 수 있는데

여기서
싱글 쓰레딩의 경우는 프로세스 내에서 단일 쓰레드만으로 작업하기 때문에
자원 분배에 있어 어려움이 없다면

멀티 쓰레딩의 경우 두개 이상의 쓰레드들이
하나의 같은 프로세스 내의 자원공유해서 작업을 하는 방식이기 때문에
서로의 작업에 영향을 미칠 수 있다.

이런 영향
즉, 간섭하는 일을 방지하기 위해서 필요한 것이 " 동기화 (synchronization) "이다.


🧿 동기화 (synchronization)

: 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것

우선 이러한 간섭 현상을 방지하기 위해선
" 한 쓰레드가 특정 작업을 끝마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 하는 것 " 이
먼저 필요하다.

쓰레드들에게 매 순서마다 주어진 작업시간은 OS 스케줄러만 알 수 있다.
이러한 특징때문에

"""
만약 예를 들어 하나의 값을 사용하는 메서드의 경우
값의 변화를 주는 메서드라면 " 중간에 끊기면 안되는 작업 "일 것이다.

그럼에도 불구하고
한 쓰레드가 실행 조건을 통과하고 값의 변화를 주는 작업을 하려는 순간
중간에 할당된 작업시간이 지나 끊기게 되면
다음 순서 올 때까지 변화를 주기 직전 그 상황인 채로 기다려야 하는데

그 사이에 다른 쓰레드가 들어와 해당 작업을 수행하게 되면
중간에 끊겼던 쓰레드의 순서가 돌아왔을 땐, 처음 접근했을 때의 그 값과 다른 값으로 변해있는 문제가 발생한다.

"""

위와 같은 문제가 발생했을 때,
어떤 쓰레드가 특정 작업에 들어가게 된다면
중간에 끊기더라도 다른 쓰레드가 그 데이터를 건드리는 작업 못하도록

먼저 들어간 쓰레드가 작업을 마칠 때까진
그 데이터를 사용하는 메서드를 잠궈버려야 해결이 될 것이다.


이를 위해 도입된 개념 ▶ 「 임계 영역 (critical section) 」과 「 잠금 (lock) 」 이다.

  • " 공유되는 데이터 "를 사용하는 (코드)영역을 " 임계 영역으로 지정 "해 놓기

  • 해당 공유데이터 (객체)가 " 가지고 있는 잠금(lock) "을 획득한 " 단 하나의 쓰레드 "만 해당 임계 영역 내의 코드를 모두 수행하고 벗어난 후, lock을 반납

  • 반납된 lock을 다음 순서 쓰레드가 획득하여 임계 영역 내의 코드를 수행

위와 같은 과정으로 동기화를 시키게되고

이 임계영역은 앞서 언급했듯이
락(lock)을 얻은 단 하나의 쓰레드만 출입 가능하고
당연히 " 객체 1개에 락 1개만 할당 "된다.

🧱 synchronized 동기화

/* 2가지 상황의 임계 영역 지정 */

/* 1.메서드 전체 → 임계영역 */
public synchronized void clacSum() {
	// 임계 영역 실행 코드
    ...
}

/* 2.특정 영역 → 임계영역 */
synchronized( 객체 참조변수 ) {
	// 임계 영역 실행 코드
    ...
}

위와 같이 synchronized 키워드로 영역을 설정하는 방법에 2가지가 있다.

첫 번째로는
메서드 앞에 붙이는 방법이다.

메서드 앞에 붙이게 되면 " 메서드 전체 == 임계영역 "으로 설정이 된다.
( 호출 : lock 부여종료 : lock 반환 )


두 번째 방법으로는
메서드 내코드 일부synchronized키워드 블럭으로 감싸는 방법이다.
이 방법을 사용할 땐
" 락을 걸고자하는 객체 참조변수 "를 매개변수로 입력해주어야 한다.
( " synchronized( [lock 객체 참조변수] ) " )

이 블럭을 " synchronized 블럭 "이라고 칭하며

"""
블럭의 영역 안으로 들어가면서부터
지정된 객체의 lock 획득하고 작업을 수행하고
마친 후, 블럭을 벗어나면 lock 반납이 된다.
"""

획득과 반남은 모두 자동적으로 이루어지기 때문에
사용자는 영역만 설정해주면 된다.

임계영역이 너무 광범위하게 되면 멀티 쓰레드 프로그램의 성능이 떨어질 수 있기 때문에
메서드 전체보다는 synchronized블럭을 통해 임계영역을 최소화하는 것이 바람직하다.


synchronized블럭을 사용해서 동기화 지원도 가능하지만
JDK 1.5 부터는 java.util.concurrent.locksjava.util.concurrent.atomic 패키지를 통해 다양한 동기화 방식 구현 가능


wait()notify()

앞서 synchronized를 통해 동기화해서 공유데이터보호하는 방법을 알아보았다.

특정 쓰레드가 작업이 끝날 때까지 특정 객체의 락(lock)을 획득한 상태에서는
다른 쓰레드가 해당 객체를 사용할 수 없게끔 하였는데

보호까지는 좋지만
이렇게 될 경우,
다른 말로 바꿔말하면 락을 가진 쓰레드가 락을 반납하기 전까지
해당 객체의 락을 기다리는 모든 쓰레드들의 작업들은 원활히 진행되지 않는 문제
가 발생한다.

이러한 문제를 개선하기 위해 출현한 것이 wait()notify() 이다.

  • 동기화된 임계 영역의 코드를 수행하다가 "작업을 더 이상 진행할 수 없는 상황" ▶ wait()
    → 쓰레드가 락을 반납하고 대기 : 락의 주인 객체의 waiting pool에 넣는다.

  • 다른 쓰레드가 락을 얻어 해당 객체에 대한 작업을 수행

  • 다시 작업을 할 수 있는 상황notify()
    → 작업을 중단햇던 쓰레드가 다시 락을 획득하고 작업 진행 : waiting pool 퇴장

class Account {
	int balance = 1000 ;
    
    public synchronized void withdraw( int money ) {
    	/*
        출금 작업을 하려는 쓰레드가 가져온 출금액이 " 잔고 보다 높으면 " wait()
        아래에서 notify()를 통해 
        해당 쓰레드가 다시 lock을 얻어서 다시 while문을 실행되고
        아직도 잔고보다 높으면 wait() 재실행
        잔고보다 작게되면 balance -= money 실행
        */
        while ( balance < money ) { 
        	try {
            	wait(); // 걸린 쓰레드는 waiting pool에 들어가고 다른 쓰레드의 작업 수행
            } catch (InterruptedException e) {}
        }
        
        balance -= money ; // bala
    }
    
    /*
    이후 다른 쓰레드 중 
    money가 balance보다 작은 경우도 lock을 얻고 withdraw()가 실행될 수도 있고
    deposit 메서드를 실행하는 쓰레드들도 실행될 수 있게 된다.
    */
    
    public synchronized void deposit( int money ) {
    	balance += money;
        notify();
    }
}

동기화 블럭(synchronized블록) 내에서만 사용할 수 있다.

  • wait() : 객체 lock 풀고 쓰레드를 해당 객체의 waiting pool에 넣음.
    매개변수가 있는 wait()으로 지정 시간 대기가 가능하다.

( 모든 객체의 waiting pool 대상이 아닌 해당 특정 객체의 waiting pool 대기중인 쓰레드 대상 )

  • notify() : 《해당 객체 waiting pool》 대기중인 쓰레드 중 하나를 깨움.

  • notifyAll() : 《해당 객체 waiting pool》 대기중인 쓰레드 모두를 깨움.

여기서 wait()notify()특정 객체에 대한 메서드로 Object 클래스에 정의되어 있다.


wait()notify()를 잘 활용하면 효율적인 동기화를 제고할 수 있는데
예를 들어

한 객체에 대해 서로 다른 두 객체가 동작을 하는 구조인데

해당 객체의 상태 혹은 가지고 있는 값에 따라서
위 서로 다른 두 객체의 동작 여부가 달라져야할 경우에

" 조건식 루프 "를 활용하여 wait() & notify()를 통한
주도권(사용 권한)을 부여/박탈 방식을 사용하면 된다.

( 단, 위 방법만으로는 notify() 되는 쓰레드를 waiting pool에서 특정해서 깨울 수 없다. ▶ " Lock&Condition " 방법 사용 : 자바의 정석 3rd 에서 출현 - 검색 필요 )

0개의 댓글