Purple Bobblehead Bunny

Backend/JAVA

[JAVA] 쓰레드 (Thread)

준영어린이 2022. 3. 10. 15:18

Thread
한번에 2개 이상의 흐름으로 프로그램을 진행하고 싶을 때 사용하는 것
(프로세스에서 프로그램의 흐름을 형성하는 것)

2개 이상의 쓰레드를 이용한 프로그램을 멀티 쓰레드 프로그램이라고 한다.

 

쓰레드 첫 번째 예제 - Thread 클래스 상속

 

 

먼저 Line8을 보면 Thread를 상속하면서 "run 메소드의 Override"가 생겼다

Thread 클래스를 상속하면 run 메소드를 오버라이드 해줘야 한다.

그리고 그 run 메소드에는 실행이 될 코드를 입력을 한다.

 

메인 클래스의  Line 24~25를 보면 run이 아니라  start를 사용하고 있다.

직접 run메소드를 호출하게 되면 단순하게 메소드만 실행이 되지만,

start를 호출하게 되면 쓰레드를 생성하여 run을 실행하게 해 준다.

바로 쓰레드를 생성하기 위해서다. (= 2개의 Fruit 객체를 각각 apple, banana로 생성하고 각각의 객체를 쓰레드화 하여

실행)

 

출력 결과를 보면, 순서대로 출력이 되지 않는다. 출력을 다시 해본 결과, 또 다르다.

쓰레드는 기본적으로 자기 멋대로 작동을 하게 된다.

 

순서대로 번갈아가면서 작동하는 것이 아닌, 쓰레드의 우선순위, 자신의 사이클에 따라서 실행이 된다.

결과를 보면, 잘 번갈아가면서 나오게 된다. 그 이유는 Line 13의 sleep(밀리세컨드)라는 메소드 때문이다.

 

쓰레드 두 번째 예제 - Runnable 인터페이스 구현

 

앞 전의 extends Thread가 implements Runnable 로 바뀌었다.

그리고 똑같이 run메소드를 오버라이드 한다.

위 예제와는 달리 sleep메소드를 사용할 때, Thread.sleep 으로 사용 했다.(Thread를 extends 안해줘서)

 

메인클래스를 보면, 이전의 예제와 달리, 쓰레드 2개를 만들어서 거기에 만든 클래스를 넣어준다.

Thread 쓰레드이름 = new Thread(연동시킬 객체);

 

이 경우 Runnable을 implements 된 클래스를 쓰레드로 연동 시켜 주는 것.

 

Runnable을 따로 만든 것은 아마 다중 상속을 하기 위함일 것 같음

만약 extends로 Thread를 상속하게 되면 다른 클래스를 상속할 수 없게 된다.

하지만 implements로 Runnable을 구현하게 되면 다른 클래스 하나를 상속할 수 있게 된다.

즉, 이런 예제가 가능하다.

 

쓰레드 우선 순위

쓰레드는 우선 순위가 있다. 그 우선 순위를 기준으로 쓰레드를 실행하게 된다.(1 최저, 10 최대)

우선순위 관련 메소드
우선순위 설정 메소드 : setPriority(순위);
우선순위 확인 메소드 : getPriority();

 

ThreadPriorty 클래스를 보면,

생성자에서 name과 우선순위에 쓰일 정수 priority를 입력받고 이름은 name, 우선순위는 setPriority에 인수로 넣게 된다.

그리고 이 쓰레드가 활성화가 되면 해당하는 클래스에 저장된 이름과 우선순위를 출력 하게 된다(getPriority)

 

메인 클래스는 각각의 ThreadPriority를 만들어 주고 각각 우선순위를 10, 5, 1로 정해두고 쓰레드화 한다.

 

우선순위를 10으로 설정한 T1을 3번(for문 i<3;) 먼저 출력하게 되고, 그 다음 순서대로 출력을 하게 된다.

 

 

만약 우선순위가 모두 같다고 하면, 

예를들어 for문을 3번이 아닌 500번을 돌리게 된다면 순서가 엉키게 된다.

 

쓰레드 주기 및 동기화

 

 

 

int형 덧셈을 하는 덧셈 프로그램이다. 

생성자로부터 두개의 int형 값을 입력 받아 쓰레드가 실행이되면 두개를 더한 값이 result 변수에 들어가고

getResult() 메소드를 호출하면 그 결과값을 출력한다.

 

근데 여기서 결과값이 0이 나오게 된다. 이유는

메인 쓰레드가 먼저 종료되었기 때문이다.

 

위 프로그램에서 쓰레드 상황은 이렇게 된다.

그런데 이 과정에서 문제가 생기게 되는데,

빨간색은 메인 쓰레드 실행 방향이고, 초록색은 CalcThread 쓰레드의 실행 방향이다.

 

쓰레드는 자신의 사이클 안에서 자기 마음대로 작동을 하게 된다.

여기서 CalcThread를 생성 후 CalcThread의 run 메소드가 실행 되는 것을 기다리지 않고 바로 출력을 해버린다.

메인 쓰레드가 CalcThread를 기다리게 할려면 join() 메소드가 필요하다.

 

Oracle Help Center의 API 문서를 보면 

Join
public final void join()
               throws InterruptedException

Wait for this thread to die. 
( 쓰레드가 끝나기를 기다린다. )

An invocation of this method behaves in exactly the same way as the invocation join(0)
( 이 메소드의 호출은 다음 호출(join(0))과 동일한 방식으로 동작한다.)

ThrowsL (예외 Throws)
InterruptedException - if nay thread has interrupted the current thread. The interrupted status of the current thread is
cleared when this exception is throws. (InterruptedException을 Throw 해야한다)

try {

    해당 쓰레드.join();

} catch (InterruptedException e) {

    예외 내용

}

해당 쓰레드가 종료될 때 까지 기다리게 된다.

 

위 예외에서 join() 메소드만 추가를 했다.(Line 21)

 

메인 쓰레드가 CalcThread가 종료될 때 까지 기다렸다가 출력을 해서 결괏값이 제대로 출력이 된다.

 

쓰레드 라이프 사이클( Thread Life Cycle )

New 쓰레드가 처음 생성(new) 된 상태를 가리켜 New라고 한다.
Runnable 쓰레드가 생성되어 start된 상태로 실행될 준비가 된 상태를 Runnable이라고 한다.
Running Runnable 상태에서 run 된 상태를 Running이라고 한다.
Waiting Running 도중에 잠깐 중단된 상태(종료X), 대기 상태를 Waiting 이라고 한다.
(Waiting 또는 Blocked 또는 Non-Runnable 이라고도 한다)
Dead run이 완료되어 끝난 상태를 Dead라고 한다.

 

쓰레드의 동기화(Synchronized)

쓰레드의 동기화는 주로 쓰레드가 2개 이상일 때 사용하는 것.

 

쓰레드는 서로 값을 공유할 수 있다. 바로 힙영역(Heap)을 이용하는 것이다.

 

힙 영역 ?

 

자바는 JVM이라 하는 가상머신 위에서 프로그램이 돌아간다.

JVM이 프로그램을 실행하기 위해 메모리를 구성하게 되는데, 다음 사진과 같이 구성하게 된다.

스택(Stack) 메소드에서 사용하는 각종 값들, 리턴 값들 저장
힙(Heap) 실행되는 클래스에서만 실행 가능하고, new로 인스턴스화 된 객체들이 저장
(GC를 사용하여 메모리 반환)
클래스(or 메소드) 클래스에서 사용되는 내용들이 저장(메소드, 자잘한 변수들의 이름 등등..)
Native Method Stack 자바 외의 다른 언어로 구현된 메소드를 위한 스택 공간
Register JVM에서 생성한 명령들의 주소를 저장

우리가 어떤 클래스를 new 하게 되면 그 클래스는 힙공간에 상주하게 된다.

 

 

힙 공간을 이용한 예제이다.

 

Num이라는 클래스는 숫자를 값으로 가지고 있으며 증가 메소드(increadNum)와 출력하는 메소드(getNum)로 구성되어 있다.

Adder라는 클래스는 위에서 선언한 Num클래스와 반복할 횟수(loop), 해당 클래스의 이름(name)을 값으로 가지고,

run메소드와 출력하는 메소드(getResult)를 가지고 있다.

메인 클래스에서 Num 클래스를 인스턴스화 하여 5라는 값을 줬다. 

그리고 Adder 클래스를 adder1, adder2라고 각각 선언 후, 5와 6만큼 loop하게 만들었다.

다음과 같은 결과를 얻게 되는데, adder1에서는 5를 5번 증가해서 10이고, adder2에서는 10에서 6번 증가해서 16 이지만, 사용한 Num 클래스인 num을 adder1과 adder2가 공유하고 있으므로,adder2에서 adder1 값 10을 받아들이 듯이

adder2에서 6번 증가한 값도 adder1에서도 16으로 받아들이게 된다.

 

동기화 ? 
MultiThread로 작업을 할 때, Thread 간의 작업이 간섭되지 않도록 하는 것

            MuitiThread 프로세스의 겨웅 여러 Trhead가 같은 자원을 공유하게 되기 때문에 이상한 결과를 불러 올 수 있다.
            A와 B가 같은 메소드를 사용한다면, B의 메소드 사용이 A의 결과에 영향을 미치게 된다
동기화 선언 방법
1. 메소드 동기화(메소드 전체를 동기화)
public synohronized void 메소드() {
      내용...
}

2. 구간 동기화(특정 구간만 동기화)
public void 메소드() {
        내용...
        synohronized(this) {
        동기화 내용..
       }
}

동기화를 할 때는 synohronized 키워드를 사용한다.

 

예제

 

여기서 티켓 수량은 한정되어 있다. 재고량이 0보다 작으면 티켓팅을 할 수 없다.

ticketNumber가 0보다 클 때만 티켓팅 성공 메세지를 출력한다.

그러나, 출력 결과에는 A에서 티켓팅을 성공하고 티켓수는 0인데 B도 티켓팅 성공 메세지가 출력이 된다.

 

예를 들어서 A가 먼저 티켓팅 메소드에 접근할 때, 현재 티켓넘버는 1이므로 A는 if문을 통과한다.

하지만 A의 티켓팅 성공으로 티켓의 수를 하나 감소시키기 전에

바로 다른 스레드를 통해 B도 if문을 통과, C도 if문을 통과해서 티켓팅이 할 수 있다.

실제로 이런 일이 벌어지면 물건의 재고와 맞지 않게 주문이 되거나 갱졍률이 치열한 티켓팅을 하는 상황이 예다.

이러한 의도를 막기 위해 동기화를 사용한다.

 

public void ticketing() --> public synchronized void ticketing() 으로 변경

 

정상적인 결과가 나오게 된다.

 

동기화 친구들 ( sleep, notify, nofityAll, wait)

sleep(long millis) 시스템의 타어미를 기준으로 millis초(ms)만큼 수면을 취한다.
notify() wait 상태로 들어간 쓰레드 1개를 깨운다.
notifyAll() wait 상태로 들어간 모든 쓰레드를 깨운다.
wait() 현재 쓰레드를 wait 상태(대기 상태)로 만든다.

쓰레드를 wait으로 대기상태로 만든 것을 다시 활성화 하기 위해서, notify 또는 notifyAll을 사용하여 깨운다.

'Backend > JAVA' 카테고리의 다른 글

[JAVA] 오버로딩, 오버라이딩 (Overloading, Overrding)  (0) 2022.03.14
[JAVA] abstract , interface  (0) 2022.03.11
[JAVA] Stack, Queue, Deque  (0) 2022.03.09
[JAVA] 메뉴 구성하기  (0) 2022.03.08
[JAVA] Event Listener Button,Mouse  (0) 2022.03.08