[Java] Multi Thread와 임계영역 설정

2021. 9. 1. 20:32개발/Java

Multi Thread 사용시 주의사항


Multi Thread 사용시 병렬적인 Task 처리가 가능하단 장점이 있지만, 자원을 공유하기 때문에 사용시 몇가지 주의할 점이 있다.

 

1. 교착상태

- 두개의 쓰레드가 서로 끝나기를 기다리는 상태이다.  자신이 원하는 자원을 상대방이 가진채로 놓지 않는 상황이다.  

 

2. 동시접근

- 특정 연산을 수행하고 있는 와중에 다른 쓰레드가 연산의 결과를 조회하는 로직을 수행하는 경우. 예상과 전혀 다른 결과가 나올 수 있다. 연산의 결과가 끝날때 까지는 다른 쓰레드가 연산결과를 조회하지 못하도록 해야한다.

 

3. 기아

- 특정 쓰레드가 계속해서 자원을 사용하지 못한채로 존재할 수 있다.

 

위 문제를 해결하기 위해 자바에선 synchronized, wait(), notify() 등 임계영역 설정과 관련된 기능을 제공한다.

 


 

Multi Thread와 임계영역 사용 예시


직접 소스코드를 작성하며 사용법을 익혔다.

 

 

 티켓 채우기와 발행을 담당하는 TicketSystem 클래스를 작성한다. publishTicket 메소드와 addTicket 메소드에 synchronized를 적용함으로써 해당 메소드를 임계영역으로 설정한다. ( 자바의 Object에는 Lock이 1개씩 존재하는데 임계영역으로 들어온 쓰레드는 Lock을 소유하게 되고 다른 쓰레드는 Lock이 없기때문에 임계영역에 진입하지 못한채로 대기하게 된다. )

 

 publishTicket 메소드에선 발급 가능한 수량이면 티켓을 발급한다. 발급조건을 만족하는 경우 1초의 대기시간을 가진뒤 남은 수량에서 요청수량을 뺀다. 

티켓발급이 불가능할 경우 Thread의 wait 메소드를 실행한다. wait 메소드는 현재 publishTicket를 사용중인 쓰레드가 클래스의 lock을 반납하고, 해당 위치에서 대기하는 상태가 되도록 한다.

 

 addTicket 메소드에선 입력받은 수만큼 티켓을 채워넣는다. 티켓발급후 notifyAll 메소드를 실행하는데, notifyAll은 TicketSystem을 사용하다 대기상태가 된 쓰레드를 모두 깨운다. notify 메소드를 사용하면 대기상태에 있는 쓰레드중 하나만 깨운다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class TicketSystem{
 
    private int remainedTickets; // 남은 티켓
    private final int MAX_TICKETS = 7// 7장까지만 채울수 있도록 한다.
 
    public TicketSystem(int remainedTickets){
        this.remainedTickets = remainedTickets;
    }
 
    // 티켓 채워넣기
    public synchronized void addTicket(int tickets){
 
        this.remainedTickets += tickets;
        if(remainedTickets > MAX_TICKETS){
            remainedTickets = MAX_TICKETS;
        }
        notifyAll();
 
    }
 
    // 티켓 발급하기
    public synchronized void publishTicket(int tickets , String cusomerName) throws InterruptedException {
 
        while(true){
 
            if(this.remainedTickets > tickets && this.remainedTickets > 0){
                Thread.sleep(1000);
                this.remainedTickets =  this.remainedTickets - tickets;
                System.out.println(cusomerName + "에게 " + tickets + "장 발권 완료했습니다. " + " 남은 티켓은 " + this.remainedTickets + "장 입니다.");
                break;
            } else { // 티켓이 없으면 기다림
                wait();
            }
 
        }
 
    }
}
 
cs

 

 

TicketSystem의 addTicket 메소드를 사용할 Staff 클래스를 작성한다. 

Staff는 workDone이 true가 되기전까진 계속해서 1장씩 티켓을 시스템에 등록한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Staff extends Thread{
 
    private TicketSystem ticketSystem;
    private boolean workDone = false;
 
    public Staff(TicketSystem ticketSystem){
        this.ticketSystem = ticketSystem;
    }
 
    public void setWorkDone(boolean workDone) {
        this.workDone = workDone;
    }
 
    @Override
    public void run() {
 
        // 직원이 티켓을 시스템에 등록한다
        while(!workDone){
            ticketSystem.addTicket(1);
        }
    }
}
cs

 

 

TicketSystem의 publishTicket 메소드를 사용할 Customer 클래스를 작성한다. 생성자를 통해 threadGroup을 전달받아 부모 클래스로 넘김으로써 Thread Group 등록작업을 수행한다.

Thread 실행시에는 5장의 티켓을 발행요청 하도록 코딩했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Customer extends Thread{
 
    private TicketSystem ticketSystem;
    private String customerName;
 
    public Customer(TicketSystem ticketSystem, ThreadGroup threadGroup,String threadName){
        super(threadGroup,threadName);
        this.customerName = threadName;
        this.ticketSystem = ticketSystem;
    }
    
    @Override
    public void run() {
        // 5장 발행요청
        try {
            ticketSystem.publishTicket(5 , this.customerName);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
 
    }
}
cs

 

 

 

이제 Main 클래스를 작성한다. 

 

10개의 customer와 1개의 staff 인스턴스를 생성 및 실행한다. customer의 쓰레드가 모두 종료되었는지 확인하기 위해 Thread group으로 묶었다. ThreadGroup.activeCount 메소드를 통해 활성화된 쓰레드 갯수를 계속해서 확인하는 작업을 수행한다. 활성화된 customer 쓰레드 개수가 0개면 staff 쓰레드도 종료한다. 

 

staff.interrupt() 를 사용하지 않은 이유는?  interrupt 메소드는 종료하려고 하는 쓰레드가 WAIT 상태를 가지고 있어야하는데 Staff 클래스의 run 메소드를 보면 while문을 통해서 계속해서 티켓을 넣는 작업을 하고 있기때문에 Runnable 상

태이다. 따라서 staff.interrupt() 코드를 작성해도 staff는 종료되지 않았기 때문에 workDone 필드를 추가했다. 

 

( 별도 Thread Group으로 묶는 작업을 하지않은 staff 인스턴스는 main Thread Group에 속하게 된다. )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.util.ArrayList;
 
public class Main {
 
    public static void main(String[] args) throws InterruptedException {
 
        // 티켓시스템 생성
        TicketSystem ticketSystem = new TicketSystem(0);
 
        // 스탭 쓰레드 1개 생성
        Staff staff = new Staff(ticketSystem);
        staff.start();
 
        // Customer의 인스턴스를 담을 List
        ThreadGroup customerGroup = new ThreadGroup("customerGroup");
        // 10개 고객 쓰레드 생성과 시작
        for(int i=0; i<10; i++){
            String customerName = "customer".concatString.valueOf(i)); // 쓰레드 이름 생성
            Customer customer = new Customer(ticketSystem,customerGroup , customerName); // 고객쓰레드 생성
            customer.start();
        }
 
 
        // 고객 쓰레드가 모두 종료된 경우 스탭 쓰레드를 종료한다.
        while(true){
            // customerGroup내 활성화된 쓰레드가 0개 인경우
            if(customerGroup.activeCount() == 0){
                staff.setWorkDone(true);
                staff.join();
                break;
            }
        }
    }
}
 
cs

 

실행하면 발권을 성공적으로 수행하고, 모든 쓰레드가 종료되어 프로세스가 finished 되었다는 문구를 볼 수 있다.

 

 

 

만약 TicketSystem 클래스에서 synchronized와 wait(), notifyAll()을 제거하면 어떤 결과가 나올까??

기존의 TicketSystem 클래스를 아래와 같이 수정했다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class TicketSystem{
 
    private int remainedTickets; // 남은 티켓
    private final int MAX_TICKETS = 7// 7장까지만 채울수 있도록 한다.
 
    public TicketSystem(int remainedTickets){
        this.remainedTickets = remainedTickets;
    }
 
    // 티켓 채워넣기
    public /*synchronized*/ void addTicket(int tickets){
 
        this.remainedTickets += tickets;
        if(remainedTickets > MAX_TICKETS){
            remainedTickets = MAX_TICKETS;
        }
        //notifyAll();
 
    }
 
    // 티켓 발급하기
    public /*synchronized*/ void publishTicket(int tickets , String cusomerName) throws InterruptedException {
 
        while(true){
 
            if(this.remainedTickets > tickets && this.remainedTickets > 0){
                Thread.sleep(1000);
                this.remainedTickets =  this.remainedTickets - tickets;
                System.out.println(cusomerName + "에게 " + tickets + "장 발권 완료했습니다. " + " 남은 티켓은 " + this.remainedTickets + "장 입니다.");
                break;
            } else { // 티켓이 없으면 기다림
                //wait();
            }
 
        }
 
    }
}
 
cs

 

실행결과에서 알수 있듯이 쓰레드들이 동시에 TicketSystem 클래스의 메소드에 접근하기 때문에 결과를 전혀 예상할수 없게 된다. remainedTickets필드가 사용되는 addTicket 메소드와 publishTicket 메소드에 쓰레드가 마구잡이로 접근하기 때문에 remainedTickets이 7이 넘거나, 음수가 나오는 케이스도 발생한다.

 

customer1이 if문 조건을 만족하고 1초 대기하는 사이, 이미 customer2도 if문의 조건을 만족하고 진입하는 케이스.

 

'개발 > Java' 카테고리의 다른 글

다형성이 적용된 Object로 JSON 전환하기  (0) 2022.09.10
[Java] Lambda Expression Quick View  (0) 2021.12.20