우주먼지
article thumbnail
Published 2023. 3. 1. 13:59
Thread Languages/Java

💡 Thread

데이터와 어플리케이션이 확보한 자원을 활용하여 소스코드 실행 즉, 하나의 코드 실행 흐름

 

Thread

  • 프로세스에서 실행 제어만 분리한 것
  • 프로세스로부터 자원을 할당받고 그 자원을 이용하는 실행의 단위
  • 프로세스의 Stack만 할당받고 코드 & 데이터 & 힙영역은 공유하기 떄문에 좀 더 효율적인 통신 가능
  • 캐시 메모리를 비우지 않아도 되서 더 빠름
  • 자원 공유로 인한 문제가 발생할 수 있으니 이를 염두에 둔 프로그래밍을 해야함
  • 한 프로세스에 여러개의 스레드가 생성될 수 있음
  • 즉, 캐시 메모리나 PCB에 저장해야하는 내용이 적고 비워야 하는 내용도 적기 때문에 상대적으로 더 빠른 컨텍스트 스위칭


💡 구현과 실행

 

Thread 클래스를 상속하는 방법 (다른 클래스 상속 불가, 권장 X)

  • Thread 클래스를 상속받은 자손 클래스의 인스턴스 생성
  • Thread 클래스를 상속받으면 메서드 직접 호출 가능
/* Thread의 run()을 오버라이딩 */
public class ExtendsThread extends Thread {
    public void run() {
        // 상속받은 Thread의 getName() 호출
        for (int i=0; i<5; i++) {
            System.out.println(getName());
        }
    }
}

 

Runnable 인터페이스를 구현하는 방법 (재사용성 & 일관성 좋음, 권장)

  • Runnable 인터페이스를 구현한 new ImplementsRunnable 인스턴스를 생성한 뒤,
    이 인스턴스를 Thread 클래스의 생성자 파라미터로 넘겨줘야 한다.
  • Runnable을 구현했을때 Thread의 메서드를 호출 하려면
    static Method인 currentThread()를 호출하여,
    스레드에 대한 참조를 얻어 와야만 호출이 가능하다.
/* Implements Runnable Interface */
public class ImplementsRunnable implements Runnable{
    @Override
    public void run() {
        for (int i=0; i<5; i++) {
            // Thread.curruneThread() - 현재 실행중인 Thread를 반환한다.
            System.out.println(Thread.currentThread().getName());
        }
    }
}

Main 메서드

public class ThreadEx {
    public static void main(String[] args) {
        // 1. Thread의 자손 클래스 인스턴스 생성
        ExtendsThread t1 = new ExtendsThread();

        // 2. Runnable을 구현한 클래스의 인스턴스 생성
        // 생성자 Thread(Runnable target)
        Thread t2 = new Thread(new ImplementsRunnable());

        t1.start();
        t2.start();
    }
}

 

실행

start()

  • start()가 실행되었다고 바로 실행되는것이 아닌 순서가 와야 실행이 된다.
  • 만약 스레드가 종료되고 1번 더 실행되어야 한다면 스레드를 다시 생성해야 한다.
ExtendsThread t1 = new ExtendsThread();
t1.start();
t1 = new ExtendsThread();
t1.start();

💡 Start()와 Run()의 차이

새로 생성한 스레드에서 고의로 예외를 발생시키고 printStackTrace()를 이용해서,
예외가 발생한 당시의 호출스택을 출력하는 예제이다.

start()

  • 새로운 스레드가 작업을 실행하는데 필요한 호출스택을 생성한 후 run()을 호출해서,
    생성된 호출스택에 run()을 올린다.
  • 호출스택의 첫번째 메서드가 main이 아닌 run 메서드이다.
  • 한 스레드에 예외가 발생해서 종료되어도 다른 스레드에 영향을 미치지 않는다.
  • 실행결과에 main 스레드의 호출스택이 없는 이유는 main 스레드가 종료되었기 때문이다.
/* Thread의 run()을 오버라이딩 */
public class ExtendsThread extends Thread {
    public void run() {
        throwException();
    }

    public void throwException() {
        try {
            throw new Exception();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
public class ThreadEx {
    public static void main(String[] args) {
        // Thread의 자손 클래스 인스턴스 생성
        ExtendsThread t1 = new ExtendsThread();
        t1.start();
    }
}

실행 결과

java.lang.Exception
    at thread.ExtendsThread.throwException(ExtendsThread.java:15)
    at thread.ExtendsThread.run(ExtendsThread.java:6)

 

run()

  • main 메서드에서 run()을 호출하는 것은 생성된 스레드를 실행시키는것이 아닌,
    다순 클래스에 선언된 메서드를 호출하는 것일 뿐이다.
  • 위의 예제와 달리 스레드가 새로 생성이 되지 않았고 그냥 run()이 실행되었을 뿐이다.
  • 호출스택에 main 메서드가 포함되어 있음
/* Thread의 run()을 오버라이딩 */
public class ExtendsThread extends Thread {
    public void run() {
        throwException();
    }

    public void throwException() {
        try {
            throw new Exception();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
public class ThreadEx {
    public static void main(String[] args) {
        // Thread의 자손 클래스 인스턴스 생성
        ExtendsThread t1 = new ExtendsThread();
        t1.run();
    }
}

실행 결과

java.lang.Exception
    at thread.ExtendsThread.throwException(ExtendsThread.java:15)
    at thread.ExtendsThread.run(ExtendsThread.java:6)
    at thread.ThreadEx.main(ThreadEx.java:7)

💡 Thread 이름 조회

스레드의참조값.getName()

Thread thread3 = new Thread(new Runnable() {
    public void run() {
        System.out.println("Get Thread Name");
    }
});

thread3.start();
System.out.println("thread3.getName() = " + thread3.getName());
Get Thread Name
thread3.getName() = Thread-0

💡 Thread 이름 설정

스레드의참조값.setName()

Thread thread4 = new Thread(new Runnable() {
    public void run() {
        System.out.println("Set And Get Thread Name");
    }
});

thread4.start();
System.out.println("thread4.getName() = " + thread4.getName());

thread4.setName("First Thread");
System.out.println("thread4.getName() = " + thread4.getName());

💡 Thread 동기화

멀티스레드의 경우, 두 스레드가 동일한 데이터를 공유하는 과정에서 문제가 발생하여 동기화 필요

 

멀티스레드의 데이터 공유 과정중 발생하는 문제에 대한 예시

try { Thread.sleep(1000); } catch (Exception error) {} 에 대한 설명을 읽고 예시를 보자
- Thread.sleep(1000);

  1. 스레드를 일시정지 시키는 메소드. ※ 스레드가 정지되면 대기열에서 대기중인 다른 스레드가 실행됨
  2. Thread.sleep()는 반드시 try .. catch문의 try 블럭내에 작성
  3. 간단하게 말하면, 스레드의 동작을 1초동안 멈추는 코드

- try { ... } catch ( ~ ) { ... }

  1. 예외처리에 사용되는 문법
  2. try 블록 내 코드를 실행하다 예외&에러 발생시 catch문 내의 코드 실행
  3. Thread.sleep(1000); 의 동작을 위해 형식적으로 사용한 문법요소임

 

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" : "")
            );
        }
    }
}
Withdraw 100₩ By 김코딩. Balance : 600 
Withdraw 300₩ By 박자바. Balance : 600 
Withdraw 200₩ By 김코딩. Balance : 400 
Withdraw 200₩ By 박자바. Balance : 200 
Withdraw 200₩ By 김코딩. Balance : -100 
Withdraw 100₩ By 박자바. Balance : -100 

위의 예제는 멀티스레드 생성 후 1000원의 잔액을 가진 계좌에서 100~300원을 인출하며, 인출금&잔액을 출력한다.
이 멀티 스레드는 Account 객체를 공유하게 된다.


💡 Critical Section & Lock

임계영역은 하나의 스레드만 코드를 실행할 수 있는 코드의 영역을 의미한다
락은 임계영역을 포함하는 객체에 접근할 수 있는 권한을 의미한다

 

메서드 전체를 임계영역으로 지정하는 예시

class Account {
    ...
    public synchronized boolean withdraw(int money) {
        if (balance >= money) {
            try { Thread.sleep(1000); } catch (Exception error) {}
            balance -= money;
            return true;
        }
        return false;
    }
}

특정영역을 임계영역으로 지정

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;
            }
    }
}

💡 Multi-Thread Programming

하나의 프로세스에서 여러개의 스레드를 만들어 자원의 생성과 관리의 중복을 최소화하는것을 의미한다

즉, 프로그램을 여러개 키는것 보다 하나의 프로그램에서 여러 작업을 해결하는 것

  • 장점
    • 멀티 프로세스에 비해 메모리 소모가 줄어듬 (자원의 효율성 증대)
    • 스레드 간 데이터를 주고받는것이 간단해지고 시스템 자원 소모가 줄어들고 그로인해 프로세스의 컨텍스트 스위칭보다도 빠르다
    • 힙 영역을 통해서 스레드간 통신이 가능, 프로세스간 통신보다 간단함
  • 단점
    • 힙 영역에 있는 자원을 사용할 때는 동기화를 해야함
    • 동기화를 위해 락을 과도하게 사용하면 성능이 저하될 수 있음
    • 하나의 스레드가 비정상적으로 동작하면 다른 스레드도 종료될 수 있음
    • **스레드 간의 자원 공유는 전역 변수(데이터 세그먼트)를 이용하므로 함께 사용할 때 충돌 발생 가능**

💡 Thread-Safe

두 개 이상의 스레드가 Race Condition(경쟁 상태)에 들어가거나 같은 객체에 동시에 접근해도 연산결과의 정합성이 보장될 수 있게끔 메모리 가시성이 확보된 상태를 의미함

'Languages > Java' 카테고리의 다른 글

Stream  (0) 2023.03.01
Single & Multi Thread  (0) 2023.03.01
J2EE  (0) 2023.02.20
Lambda  (0) 2023.02.19
ObjectMapper  (0) 2023.01.09
profile

우주먼지

@o귤o

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

검색 태그