자바

[JAVA] 스레드(Thread)

ChaeHing 2023. 3. 10. 19:22

스레드(Thread)

  • 실행중인 애플리케이션을 프로세스(Process)
  • 프로세스는 데이터, 컴퓨터 자원, 스레드로 구성
  • 프로세스 내에서 실행되는 소스 코드의 실행 흐름스레드(Thread)
  • 스레드는 데이터와 애플리케이션이 확보한 자원을 활용하여 소스 코드를 실행
  • 하나의 스레드를 가지는 프로세스를 싱글 스레드 프로세스
  • 여러 개의 스레드를 가지는 프로세스를 멀티 스레드 프로세스

메인 스레드(Main Thread)

  • 자바 프로그램 실행시 메인이 되는 스레드
  • 메인 메서드는 제일 처음 main 메서드를 실행시킨다.
  • main 메서드의 코드 처음부터 끝까지 순차적으로 실행하여, main 메서드의 코드의 끝을 만나면 스레드가 종료된다.
  • 중간에 다른 스레드를 생성하지 않고 메인 스레드로만 프로세스를 실행하는 경우 싱글 스레드 프로세스

멀티 스레드(Multi Thread)

  • 하나의 프로세스는 여러개의 스레드를 가질 수 있다.
  • 여러개의 스레드를 가지는 경우 멀티 스레드 프로세스 이다.
  • 여러개의 스레드를 가진경우 여러 스레드가 동시의 작업을 할 수 있다. 이 멀티 스레딩이라고 한다. 
    • 병렬 작업이 가능하다.
  • 하나의 애플리케이션에서 여러개의 작업을 동시에 가능하게 해준다.
    • 메신저 프로그램에서 음악을 들으며 메시지를 보낼수 있다.

 

작업 스레드의 생성과 실행

  • 스레드를 생성한다는 것은 메인 스레드외 별도의 작업스레드를 생성한다는 것을 의미한다.
  • 자바는 객체지향 언어이기 때문에 클래스 안에 run()이라는 메서드를 정의해야 한다.

작업 스레드 생성 방법 두가지

  • Runnable 인터페이스를 구현한 객체에서 run()을 구현하여 스레드를 생성
  • Thread 클래스를 상속받은 하위클래스에서 run()을 구현하여 스레드를 생성
public class ThreadEx {
    public static void main(String[] args) {

        // 1.
        Runnable task1 = new ThreadTask1(); // Runnable 인터페이스 구현한 객체 생성
        Thread thread1 = new Thread(task1); // 구현 객체를 Thread 클래스로 인스턴스화 (스레드 생성)
        thread1.start(); // 작업 스레드 실행 -> run()을 실행 시킨다.


        // 2.
        ThreadTask thread2 = new ThreadTask(); // Thread를 상속받은 클래스 인스턴스화
        thread2.start(); // 작업 스레드 실행 -> run()을 실행 시킨다.
        
        for (int i=0; i<100; i++){
            System.out.print("@"); // @ 100개 찍기
        }
    }
}




// 1. Runnable 인터페이스 구현 클래스
class ThreadTask1 implements Runnable{
    @Override
    public void run() { // run()메서드 생성
        for (int i=0; i<100; i++){
            System.out.print("*"); // * 100개 찍기
        }
        System.out.println(Thread.currentThread().getName());

    }
}

// 2. Thread 클래스 상속 클래스
class ThreadTask2 extends Thread{
    public void run(){  // run()메서드 생성
        for (int i=0; i<100; i++){
            System.out.print("#"); // # 100개 찍기
        }
        System.out.println(Thread.currentThread().getName());
    }
}



/*

****************************************************************
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@******##################******
***********************##########################################
###########

*/
  • 코드 내용상 순차적으로 실행됬다면  *, @, # 이 100개씩 순서대로 찍혀야 하지만 출력 내용을 보면 기호들이 뒤섞임
    • 작업 스레드를 생성하여 프로그램을 실행 했기 때문에 작업들이 동시에 처리
    • 메인스레드와 작업스레드가 병렬로 실행

익명 객체로 Thread 생성

Thread thread3 = new Thread(new Runnable() { // 익명 구현 객체
    @Override
    public void run() {

        for (int i=0; i<100; i++){
            System.out.print("*");
        }
    }
});

thread3.start();
Thread thread4 = new Thread() { // 익명 하위 객체
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print("#");
        }
    }
};

thread4.start();

 

스레드 이름

  • main 스레드는 "main"
  • 그외 추가적으로 생성하는 스레드 이름은 "Thread-n" 으로 생성 된다.
  • 스레드들의 이름을 불러오고, 설정 할 수 있다.

스레드의 이름 조회

  • 스레드참조명.getName() 
Thread thread3 = new Thread(new Runnable() { // 익명 구현 객체
    @Override
    public void run() {
        System.out.println("thread3");

    }
});

thread3.start();
System.out.println("thread3 name : "+thread3.getName()); // thread3 이름 출력


Thread thread4 = new Thread() { // 익명 하위 객체
    public void run() {
        System.out.println("thread4");
    }
};

thread4.start();

System.out.println("thread4 name : "+thread4.getName()); // tread4 이름출력

/*
thread3
thread3 name : Thread-0
thread4
thread4 name : Thread-1
*/

 

스레드 이름 설정

  • 스레드참조명.setName()
Thread thread3 = new Thread(new Runnable() { // 익명 구현 객체
    @Override
    public void run() {
        System.out.println("thread3");

    }
});

thread3.setName("Thread3!!"); // thread3 이름 설정
thread3.start();
System.out.println("thread3 name : "+thread3.getName()); // thread3 이름 출력


Thread thread4 = new Thread() { // 익명 하위 객체
    public void run() {
        System.out.println("thread4");
    }
};
thread4.setName("Thread4!!"); // thread4 이름 설정
thread4.start();

System.out.println("thread4 name : "+thread4.getName()); // tread4 이름출력


/*
thread3
thread3 name : Thread3!!
thread4
thread4 name : Thread4!!
*/

스레드 인스턴스의 주소값 얻기

  • 인스턴스화된 스레드의 참조명이 아닌 현재 실행중인 스레드의 이름을 가져 오는방법
  • Thread.currentThread()    -> Thread 클래스의 정적메서드
Thread thread5 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()); // 현재 스레드 이름 출력
    }
});

thread5.start();
System.out.println(Thread.currentThread().getName()); // 현재 스레드 이름 출력

/*
main
Thread-0
*/

//run() 블럭안에서는 main 스레드 상태이므로 main
//thread5.start() 이후에 작업스레드가 생성되므로 이후 스레드는 Thread-0이 출력 되었다.

스레드의 동기화

  • 두 스레드가 동일한 데이터를 공유하게되는 문제 방지
package Thread;

// 버핏과 머스크가 깐부를 맺어 통장을 공유 하여 서로 돈을 인출하는 프로그램

public class ThreadSyn {
    public static void main(String[] args) {

        Runnable threadTask3 = new ThreadTask3(); // Runable 인터페이스 구현 객체
        Thread thread3_1 = new Thread(threadTask3);  // 스레드1 생성 
        Thread thread3_2 = new Thread(threadTask3); // 스레드2 생성

        thread3_1.setName("버핏");
        thread3_2.setName("머스크");

        thread3_1.start();
        thread3_2.start();

    }
}

class Acoount { // 계좌 클래스
    private int balance; //잔고

    public Acoount(int balance) {
        this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }

    public boolean withdraw(int money){ // 돈 인출 메서드
           if (balance >= money) { // 인출하려는 돈보다 잔고가 더 많다면 실행

               try {
                   Thread.sleep(1000); // 스레드를 1초가 멈춤, 문제 상황을 발생시키기 위해 사용
               } catch (Exception error) {
               }

               balance -= money; // 인출하려는 돈
               return true; // 인출후 true 반환

           }
           return false; // 인출이 불가하므로 false 반환
       }
}


class ThreadTask3 implements Runnable{   // 쓰레드
    Acoount account = new Acoount(100000); // 계좌 객체 생성, 십만달러 입금

     public void run(){
        while(account.getBalance() > 0){

            int money = (int)(Math.random() * 3+1) *10000; //  10000~30000 달러 랜덤 생성
            boolean denied = !account.withdraw(money); // 돈 인출 메서드 withdraw() 호출, 결과를 denied의 저장

            System.out.println(String.format("Withdraw %d$ By %s. Balance %d$ %s",
                    money, Thread.currentThread().getName(), account.getBalance(), denied ? "-> DENIED" : "")
                    // 결과 출력
            );
        }
    }
}

/*
Withdraw 20000$ By 머스크. Balance 70000$ 
Withdraw 10000$ By 버핏. Balance 70000$ 
Withdraw 10000$ By 머스크. Balance 40000$ 
Withdraw 20000$ By 버핏. Balance 40000$ 
Withdraw 20000$ By 머스크. Balance -10000$ 
Withdraw 30000$ By 버핏. Balance -10000$ 
*/
  • 예제의 출력 결과를 보면 결과값이 이상하다.
    • 처음 머스크가 20000$를 출금했는데 잔고가 70000$ 이다.
    • 인출하려는 돈보다 잔고가 적으면 false가 반환되어 "-> DENIED"가 출력되어야하지만
    • 출금되어 잔고가 -10000$이 된다.
  • 이러한 이유는 Account 객체를 공유하고 있는 두개의 쓰레드가 실행 되어서 그렇다. 
  • 스레드는 동시작업이 가능하기 때문에 머스크가 출금하는 시점(20000$)에, 버핏도 출금(10000$) 하여서 출력 시점엔잔고가 이미 70000$이되어 출력결과가 70000$이 되었다.
  • 같은 원리로 잔고가 -10000$ 된 이유는 먼저 실행된 스레드가 ' balance -= money'를 실행하기도 전에 'if (balance >= money)'에 다른 스레드가 통과를 하여 동시에 잔고를 소비해버렸기 때문이다.
  • 이처럼 쓰레드는 동시 작업을 하기 때문에 공유 데이터를 사용하다보면 예기치 못한 문제가 생길 수 있다.
  • 이러한 문제를 해결 하기 위해 임계영역락(Lock)을 사용한다.

임계영역(Critical Sectcion) 락(Lock)

  • 임계 영역은 오로지 하나의 스레드만 코드를 실행 할 수 있는 코드의 영역을 의미
  • 락은 임계 영역을 포함하고 있는 객체에 접근할 수 있는 권한을 의미 
  • 임계 영역내 코드를 실행하려면 락이 필요하고 락은 한개의 스레드에게만 부여된다.
  • 한개의 스레드가 작업을 끝내면 락을 반납하고 다른 스레드가 락을 획득하여 임계 영역 내 코드를 실행한다.
    • 바톤 터치와 비슷하다.

임계영역 지정

  • sychronized라는 키워드를 사용 한다.
  • 메서드 전체를 임계영역으로 지정 할 수도 있고, 특정한 부분을 임계 영역으로 지정 할 수도 있다.

메서드 전체 임계 영역 지정

// 메서드 시그니처에 synchronized 키워드 추가

public synchronized boolean withdraw(int money){ // 메서드 전체의 임계 영역 지정
       if (balance >= money) { // 인출하려는 돈보다 잔고가 더 많다면 실행

           try {
               Thread.sleep(1000); // 스레드를 1초가 멈춤, 문제 상황을 발생시키기 위해 사용
           } catch (Exception error) {
           }

           balance -= money; // 인출하려는 돈
           return true; // 인출후 true 반환

       }
       return false; // 인출이 불가하므로 false 반환
}

특정 부분 임계 영역 지정

// 지정할 영역을 synchronized (this){}로 블럭안에 지정
// this는 현재 객체를 의미함, 스레드 실행시점의 객체

public boolean withdraw(int money) { // 돈 인출 메서드

    synchronized (this) { // 특정 부분 임계 영역 지정
        if (balance >= money) { // 인출하려는 돈보다 잔고가 더 많다면 실행

            try {
                Thread.sleep(1000); // 스레드를 1초가 멈춤, 문제 상황을 발생시키기 위해 사용
            } catch (Exception error) {
            }

            balance -= money; // 인출하려는 돈
            return true; // 인출후 true 반환

        }
        return false; // 인출이 불가하므로 false 반환
    }
}

임계 영역 지정시 결과

Withdraw 10000$ By 버핏. Balance 90000$ 
Withdraw 30000$ By 머스크. Balance 60000$ 
Withdraw 10000$ By 버핏. Balance 50000$ 
Withdraw 30000$ By 머스크. Balance 20000$ 
Withdraw 20000$ By 버핏. Balance 0$ 
Withdraw 10000$ By 머스크. Balance 0$ -> DENIED
  • 정상적으로 출금한 만큼 잔고가 발생
  • 잔고보다 많은 돈을 출금하려하면 '-> DENIED' 도 발생