본문 바로가기

Java/Study

[JAVA]백기선 라이브 스터디 10주차 - Thread

목표

자바의 멀티쓰레드 프로그래밍에 대해 학습하세요.

학습할 것 (필수)

  • Thread 클래스와 Runnable 인터페이스

  • 쓰레드의 상태

  • 쓰레드의 우선순위

  • Main 쓰레드

  • 동기화

  • 데드락


프로세스 : 현재 실행 중인 프로그램 

작업 관리자에서 프로세스를 확인 할 수 있다.

쓰레드 :  실제로 작업을 수행하는 것

멀티 쓰레드:  여러 개의 프로그램이 동시에 실행된다는 의미이다.

멀티 프로세스 vs 멀티 쓰레드
멀티 프로세스는 할당받은 메모리에 독립적으로 실행 -> 다른 프로세스에 영향을 주지 않음
멀티 쓰레드는 하나의 프로세스 내부에 생성 ->  다른 쓰레드에 영향을 줌.( 프로세스 자체가 종료될 수 있음)

 


1. Thread 클래스  vs  Runnable 인터페이스

공통점 : public void run() {} 추상 메소드를 구현한다는 점

Runnable 인터페이스는 다중 상속이 가능해서 Runnable 인터페이스를 생성하는 것이 선호된다.

class ThreadRunnable implements Runnable{// 인터페이스의 run()을 구현
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName());
        }
    }
}
class ThreadClass extends Thread{// Thread클래스의 run()을 오버라이딩
    public void run(){
        for(int i = 0; i <5; i++) {
            System.out.println(getName());
        }
    }
}

 

※ 참고 : 인스턴스의 생성 방법이 다르다.

    public static void main(String[] args) {
        ThreadClass th_1 = new ThreadClass();//클래스 생성
        Runnable r = new ThreadRunnable();
        Thread th_2 = new Thread(r);
        th_1.start(); //call stack(호출스택)을 생성한 후 run()을 호출 이후에 자세히 다룰 예정
        th_2.start();
    }

다른 이유는 Runnable인터페이스는 run()만 구현하도록 정의되어 있는  간단한 인터페이스이므로  호출 스택을 생성할 start()가 없다.

main 함수에서 보여지듯이 쓰레드를 생성했다고 자동으로 실행되는 것은 아니다.

Start()를 호출해야만 쓰레드가 실행된다. 

Start()의 역할은 새로운 쓰레드가 작업을 실행하는데 필요한 호출 스택을 VM에 생성한다.

Start()를 통해 자신만의 호출 스택을 생성한 다음에 run()을 호출 스택의 첫 번째로 올린다.

쓰레드가 종료되면 작업에 사용된 호출 스택은 소멸된다.


2. 쓰레드의 상태 (생명 주기)

1) new : thread가 생성되고 아직 start()가 호출되기 전

           ↓ "start()"로 변경

2) runnable : 실행 중 또는 실행 가능한 상태

3) block : 동기화 블럭에 의해 일시 정지된 상태 (= lock)

4) timed waiting, waiting : 실행가능하지 않은 (unrunnable) 일시 정지 상태 

5) terminate : 작업이 종료된 상태

 

※ 주요 메서드 : sleep(), interrupt(), yield(), join()

sleep() : 현재 작업중인 스레드를 멈춤

interrupt() : 실행 중인 스레드를 끝나기 전에 취소시켜야할 때

yield() : 다른 스레드에 자원을 양보함

join() : 다른 스레드의 작업을 기다림  (현재 실행중인 스레드)


3. 쓰레드의 우선 순위

싱글쓰레드 프로세스의 경우, 단 하나만 작업한다.

멀티쓰레드 프로세스이 경우, 같은 프로세스 내의 자원을 공유하며 작업하므로 서로의 작업에 영향을 줌.

멀티쓰레드 프로세스는 동시성 또는 병렬성으로 실행된다.

동시성 : 하나의 코어에서 여러개의 프로세스가 번갈아 가면서 실행됨

병렬성 : 멀티 코어에서 개별 스레드를 동시에 실행

싱글 코어 CPU는 ->  동시성으로 멀티 스레드 진행 

※병렬적으로 보이는 이유는 속도가 매우 빠르기 때문

스레드 스케줄링

코어의 개수 < 스레드 개수 경우, 어떤 순서에 의해 동시성으로 실행할 것인지에 대한 결정

    public static void main(String[] args) {
    //main의 우선순위 : 5
    // main에서 실행하는 스레드는 모두 우선순위 5 우선 순위가 높은것 먼저 실행
        ThreadClass th_1 =new ThreadClass();
        th_1.setPriority(3);// 우선 순위 변경
        System.out.println(th_1.getPriority());
        ThreadRunnable th_2 = new ThreadRunnable();
        Thread thh = new Thread(th_2);
        System.out.println(thh.getPriority());
        thh.start();
        th_1.start();

    }

실행 결과,

1이 먼저 실행

 


4. Main 쓰레드

Main 쓰레드는 3.에서 설명했듯이, 멀티 쓰레드 프로그램 중 "병렬적"으로 실행되는 쓰레드이다.

Java프로그램이 실행될때, 즉시 실행되는 쓰레드가 바로 main쓰레드 이다

출처 : https://www.geeksforgeeks.org/main-thread-java/

Daemon Thread란?

우선 순위가 낮아서 배경에서 실행되고 있는 쓰레드를 의미한다.

JVM은 User Thread가 실행할 때만 존재함 ex) daemon만 실행되는 경우, 자체적으로 JVM이 종료됨

public class DemoDaemon {
    public static void main(String[] args) {
        DaemonThread dth_1 = new DaemonThread("dth_1");
        dth_1.setDaemon(true); // 이 부분을 주석처리하면
        dth_1.start();
        try{
            Thread.sleep(3000);
        }catch (InterruptedException ie){

        }
        System.out.println("메인쓰레드 종료");
    }
}

dth_1.setDaemon을 주석처리하면 ,

class DaemonThread extends Thread{
    public DaemonThread(String name) {
        super(name);
    }
    public void run(){
        while (true){//1초마다 메세지를 출력하는 메소드 루프로 돌아서 계속 반복해야 함
            try{
                Thread.sleep(1000);
            }catch(InterruptedException ie){

            }
            System.out.println("DaemonThread종료");
        }

무한 루프에 빠지게 된다. 

daemon 메서드인 경우

 

daemon메서드가 아닌 경우


5.동기화(Synchronized)

동기화 :  현재 실행 중인 쓰레드를 제외하고, 나머지 쓰레드에서는 데이터에 접근 할 수 없도록 막는 개념.

멀티 쓰레드를 잘 사용하면 성능을 증가 시키지만 ,

공유 자원(데이터)에 대한 동기화가 없는 경우 : 데이터의 안정성과  신뢰성을 보장할 수 없음!

동기화 하는 방법
public static synchronized void Method(){} // 메소드를 동기화 하는 방법

sychronized(변수){}// 변수를 동기화 하는 방법

 

class Account{
    int balance = 1000;
    public void withdraw(int money){
        if(balance >= money){
            try{
                Thread thread = Thread.currentThread();
                System.out.println(thread.getName() + "출금 금액 ->>"+ money);
                balance -= money;
                System.out.println(thread.getName() + "잔액 ->>" + balance);
            }catch(Exception e){}
        }
    }
}

코드를 보면, withdraw메서드에서 잔액을 삭감한다.

class Task implements Runnable{
    Account acc = new Account();
    @Override
    public void run() {
        while(acc.balance > 0){
            int money = (int)(Math.random()*3 + 1)*100;
            acc.withdraw(money);
        }

    }
}

잔액이 0보다 클 경우에만 잔액을 삭감하도록 한다.

main에서 쓰레드를 2개 생성 후 출금을 하면

잔액이 -로 내려가는 경우

쓰레드 2개가 balance에 동시에 접근해서 발생하는 문제이다.

※Sychronized를 남발하면 성능에 문제를 줄 수 있으므로 가능한 좁은 범위에서 사용하도록 하자

sychronized를 사용하면 멀티쓰레드가 코드를 실행할 수 없기 때문에 각 쓰레드들이 (순서는 모르고) 순차적으로 sychronized영역을 실행함. 그래서 일부만 동기화가 필요하다면 전체를 sychronized할 필요가 없음.


6. 데드락(Dead-lock)

`교착상태`라고 하며 한정된 자원을 여러곳에서 사용하려고 할 때 발생할 수 있다.

출처 : https://www.geeksforgeeks.org/deadlock-in-java-multithreading/

데드락의 발생조건

4가지 조건을 동시에 충족할 경우 

조건 내용
Mutual Exclusion(자원에 대한 동시 접근 불가) 여러 프로세스가 한 자원에 접근하지 못함
Hold and Wait(점유하고 기다리기) 자원을 가지고 있는 상태에서 다른 프로세스의 자원을 대기
No Preemption(자원 뺏어오지 못함) 다른 프로세스가 점유한 자원 우선권이 낮다
Circular Wait (순환 형태로 대기함) 대기중이라서 모순적 누군가가 일을 끝내야 수행이 가능

데드락을 예방

4가지 조건 중 하나라도  발생하지 않도록 시스템 차원에서 막아버린다.

단점 : 자원이 낭비 되고, 성능이 나빠질 수 있다.

 

데드락을 회피

발생을 막는 알고리즘을 적용해서 해결하는 방법, 

자원을 할당해도 안전한지 검사

 

데드락의 탐지와 회복

교착상태가 발생한 후 해결하려는 방법

 

데드락 무시

데드락이 발생할 확률이 낮다면 그냥 무시한다. -> 성능이 낮아진다

 

 

결론 : 안전성과 성능을 고려해서 데드락 문제를 해결하자