스레드

이용만·2023년 3월 10일
0

핵심과 기초적인 것만 학습하자
자바 심화 중 어려운 개념이기에 학습하는 기간동안은 잘 쓰이진 않을 것.
어떻게 학습을 하는가? 컨텐츠에서는 핵심적인 것만 놓았다.
스레드 실행과 주의사항, 동기화, 어떻게 하는지? 그리고 상태제어.

프로세스 : 실행중인 애플리케이션. 운영체제로부터 실행에 필요한 만큼의 메모리를 할당 받아 프로세스가 된다.
프로세스는 데이터, 컴퓨터 자원, 스레드로 구성된다.
스레드는 데이터와 애플리케이션이 확보한 자원을 활용하여 소스코드를 실행한다.
즉 스레드는 하나의 코드 실행 흐름이라 볼 수 있다.

메인스레드
자바 app을 실행하면 가장 먼저 실행되는 main 메서드를 말하고, 메인 스레드가 main 메서드를 실행시켜준다. main메서드의 처음부터 끝까지 순차적으로 실행하고, 코드의 끝을 만나거나 return문을 만나면 실행을 종료한다.
만약,소스코드가 싱글 스레드로 작성되면, 실행 후 프로세스가 될 때 메인 스레드만 가지는 싱글 스레드 프로세스가 된다.
반면 메인 스레드에서 또 다른 스레드를 생성하여 실행한다면 해당 app은 멀티 스레드로 동작된다.

멀티스레드 : 하나의 프로세스는 여러 개의 스레드를 가진다.
여러개의 스레드를 가진다 = 여러 스레드가 동시에 작업을 수행한다. = 멀티 스레딩
ex) 카카오톡 : 메신저를 보내면서 동시에 파일도 업로드할 수 있다.

🔎작업 스레드 생성과 실행

자바는 객체지향언어이다. So, 모든 코드는 크래스 내에서 작성된다.
그래서 run()이라는 메서드 내에 스레드가 처리할 작업을 작성하도록 규정됭 있기에
run()메서드는 Runnable 인터페이스와 Thread 클래스 내에 정의되어져 있다.

  • 첫 번째 방법
    Runnable 인터페이스를 구현한 객체에서 run()을 구현하여 스레드를 생성하고 실행하는 방법
  • 두 번째 방법
    Thread 클래스를 상속 받은 하위 클래스에서 run()을 구현하여 스레드를 생성하고 실행하는 방법

👉1. Runnable 인터페이스를 구현한 객체에서 run()을 구현하여 스레드를 생성하고 실행하는 방법

스레드 생성만으로는 run()메서드 실행 X. 실행하려면 start() 메서드를 호출하여 스레드를 실행한다.

public class Main {
    public static void main(String[] args) {
        //Runnable 인터페이스를 구현한 객체 생성
        Runnable task1 = new ThreadTask1();

        //Runnable 구현 객체를 인자로 전달하고 Thread 클래스를 인스턴스화하여 스레드를 생성
        Thread thread1 = new Thread(task1);

        //스레드 실행
        thread1.start();

        //반복문 추가
        for(int i = 0; i<100; i++){
            System.out.printf("@");
        }
    }
}

public class ThreadTask1 implements Runnable{
    public void run(){
        for(int i = 0; i<100; i++){
            System.out.printf("#");
        }
    }
}

-> 출력결과는 실행시마다 다르다.

@는 main 메서드에서 출력된 것,@는 메인 스레드에의 반복문 코드 실행에 의해 출력된 것.
#은 run() 메서드에서 출력된 것, #는 작업 스레드의 반복문 코드 실행에 의해 출력된 것.
즉 메인 스레드와 작업 스레드가 동시에 병렬로 실행되었다는 것이고, main 메서드와 run()메서드의 코드를 실행했기에 두가지 문자가 섞여서 출력 된 것.

**👉2.Thread 클래스를 상속 받은 하위 클래스에서 run()을 구현하여 스레드를 생성하고 실행하는 방법

public class Main {
    public static void main(String[] args) {
        //Thread 클래스를 상속받은 클래스를 인스턴스화하여 스레드를 생성
        ThreadTask2 threadTask2 = new ThreadTask2();
        threadTask2.start();
        for (int i=0; i<100; i++){
            System.out.println("@");
        }
    }
}

public class ThreadTask2 extends Thread{
    public void run(){
        for (int i = 0; i<100; i++){
            System.out.println("#");
        }
    }
}

첫번째 방법과 차이가 있는데 Thread 클래스를 직접 인스턴스화 하지 않았다.
정리하면 스레드를 실행하기 위해선 생성하고 실행을 해야하는데 자바는 객체지향언어이기에 클래스 안에(ThreadTask1,ThreadTask2) run() 메서드를 정의했다.

클래스 없이 익명 객체를 통해 스레드 생성 및 실행도 가능하다.

->Runnable 익명 구현 객체 활용

public class ThreadExample1 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i<100 ; i++){
                    System.out.println("#");
                }
            }
        });
        
        thread.start();
        
        for (int i = 0; i<100; i++){
            System.out.println("@");
        }
    }
}

->Thread 익명 하위 객체 활용

public class ThreadExample1 {
    public static void main(String[] args) {
        Thread thread = new Thread(){
            public void run(){
                for (int i = 0 ; i< 100; i++){
                    System.out.println("#");
                }
            }
        };
    }
}

ps..개인적으로 Thread 클래스를 상속받아 스레드를 생성하는게 더 가독성이 있고 편하다.


스레드의 이름 조회하기 : 스레드의 참조값.getName()


스레드의 이름 설정하게 : 스레드의 참조값.setName()


스레드 인스턴스 주소값 얻기 : 정적 메서드이기에 Thread.currentThread()

🔎스레드 동기화

두 스레드가 동일한 데이터를 공유하게 되어 문제가 발생할 수 있다.

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

        Runnable threadTask3 = new ThreadTask3();
        Thread thread3_1 = new Thread(threadTask3);
        Thread thread3_2 = new Thread(threadTask3);

        thread3_1.setName("김코딩");
        thread3_2.setName("박자바");

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

class Account {

    // 잔액을 나타내는 변수
    private int balance = 1000;

    public int getBalance() {
        return balance;
    }
		
    // 인출 성공 시 true, 실패 시 false 반환
    public boolean withdraw(int money) {

        // 인출 가능 여부 판단 : 잔액이 인출하고자 하는 금액보다 같거나 많아야 합니다.
        if (balance >= money) {

            // if문의 실행부에 진입하자마자 해당 스레드를 일시 정지 시키고, 
            // 다른 스레드에게 제어권을 강제로 넘깁니다.
            // 일부러 문제 상황을 발생시키기 위해 추가한 코드입니다.
            try { Thread.sleep(1000); } catch (Exception error) {}

            // 잔액에서 인출금을 깎아 새로운 잔액을 기록합니다.
            balance -= money;

            return true;
        }
        return false;
    }
}

class ThreadTask3 implements Runnable {
    Account account = new Account();

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

            // 100 ~ 300원의 인출금을 랜덤으로 정합니다. 
            int money = (int)(Math.random() * 3 + 1) * 100;

            // withdraw를 실행시키는 동시에 인출 성공 여부를 변수에 할당합니다. 
            boolean denied = !account.withdraw(money);

            // 인출 결과 확인
            // 만약, withraw가 false를 리턴하였다면, 즉 인출에 실패했다면,
            // 해당 내역에 -> DENIED를 출력합니다. 
            System.out.println(String.format("Withdraw %d₩ By %s. Balance : %d %s",
                    money, Thread.currentThread().getName(), account.getBalance(), denied ? "-> DENIED" : "")
            );
        }
    }
}

음수 잔액이 나온다. 이유는 두 스레드가 하나의 Account객체를 공유하는 상황이고, 한 스레드가 if문의 조건식을 true로 평가하여 if문의 실행부로 코드의 흐름이 이동하는 시점에 다른 스레드가 끼어들어 balance를 인출했기 때문이다.
그래서 스레드 동기화가 필요하고 동기화를 적용하기 위해선 임계영역이 필요하다.

임계영역 : 오로지 하나의 스레드만 코드를 실행할 수 있는 코드 영역
락 : 임계 영역을 포함하고 있는 객체에 접근할 수 있는 권한

임계 영역으로 설정된 객체는 다른 스레드에 의해 작업이 이루어지고 있지 않을 때, 임의의 스레드 A는 해당 객체에 대한 락을 획득하여 임계 영역 내의 코드를 실행할 수 있다.
(내가 여기서 이해해본 바로는 임계영역 설정된 객체의 로직이 다 수행되고 반환 값을 사용할 수 있다란 말 같다.)

->위 예제 코드의 withDraw() 메서드를 임계 영역으로 설정한다.
특정 코드 구간을 임계 영역으로 설정할 때는

synchronized

키워드를 사용한다.

✏️synchronized 사용법

  1. 메서드 전체를 임계 영역 지정
class Account {
	...
	public synchronized boolean withdraw(int money) {
	    if (balance >= money) {
	        try { Thread.sleep(1000); } catch (Exception error) {}
	        balance -= money;
	        return true;
	    }
	    return false;
	}
}
  1. 특정한 영역을 임계 영역 지정
class Account {
	...
	public boolean withdraw(int money) {
			synchronized (this) {
			    if (balance >= money) {
			        try { Thread.sleep(1000); } catch (Exception error) {}
			        balance -= money;
			        return true;
			    }
			    return false;
			}
	}
}

임계영역을 지정한 후 정상적으로 출력이 된다.

🔎(Optional) 스레드의 상태와 실행 제어

start() 메서드 : 스레드의 상태를 실행 시키는게 아닌 실행 대기 상태로 만들어준다.

  • 스레드에는 상태가 존재한다.
  • 스레드의 상태를 바꿀 수 있는 메서드가 존재한다.

스레드 실행 제어 메서드

  • sleep(밀리초) : milliSecond 동안 스레드를 잠시 멈춘다.
    정적 메서드이기에 ex) Thread.sleep(1000); 과 같이 호출한다.
    호출 시 상태 : 실행 -> 일시정지 전환

실행 대기 상태로 전환하기 위해선 시간이 경과하거나 interrupt() 호출할 경우인데 후자의 경우 예외가 발생하기에 try~catch문을 사용해 예외처리 해주어야 된다.

  • intterrupt() : 일시 중지 상태인 스레드를 실행 대기 상태로 복귀시킨다.
    sleep(),wait(),join()에 의해 일시정지 된 스레드를 "멈춰있는 스레드.interrupt()" 호출하면 예외가 발생하여 일시정지가 풀린다.

  • yield() : 다른 스레드에게 실행을 양보한다.

  • join() : 다른 스레드의 작업이 끝날때까지 기다린다.
    정적메서드가 아닌 인스턴스 메서드이기에 "thread.join()"

  • wait(), notify() : 스레드 간 협업에 사용된다.

public class ThreadExample5 {
    public static void main(String[] args) {
        WorkObject sharedObject = new WorkObject();

        ThreadA threadA = new ThreadA(sharedObject);
        ThreadB threadB = new ThreadB(sharedObject);

        threadA.start();
        threadB.start();
    }
}

class WorkObject {
    public synchronized void methodA() {
        System.out.println("ThreadA의 methodA Working");
        notify();
        try { wait(); } catch(Exception e) {}
    }

    public synchronized void methodB() {
        System.out.println("ThreadB의 methodB Working");
        notify();
        try { wait(); } catch(Exception e) {}
    }
}

class ThreadA extends Thread {
    private WorkObject workObject;

    public ThreadA(WorkObject workObject) {
        this.workObject = workObject;
    }

    public void run() {
        for(int i = 0; i < 10; i++) {
            workObject.methodA();
        }
    }
}

class ThreadB extends Thread {
    private WorkObject workObject;

    public ThreadB(WorkObject workObject) {
        this.workObject = workObject;
    }

    public void run() {
        for(int i = 0; i < 10; i++) {
            workObject.methodB();
        }
    }
}

0개의 댓글