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

1. 💡 Thread

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

 

Thread

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


2. 💡 구현과 실행

 

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

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

 

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

  • Runnable 인터페이스를 구현한 new ImplementsRunnable 인스턴스를 생성한 뒤,
    이 인스턴스를 Thread 클래스의 생성자 파라미터로 넘겨줘야 한다.
  • Runnable을 구현했을때 Thread의 메서드를 호출 하려면
    static Method인 currentThread()를 호출하여,
    스레드에 대한 참조를 얻어 와야만 호출이 가능하다.
<code />
/* 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 메서드

<code />
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(); } }

 

2.3. 실행

start()

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

3. 💡 Start()와 Run()의 차이

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

3.1. start()

  • 새로운 스레드가 작업을 실행하는데 필요한 호출스택을 생성한 후 run()을 호출해서,
    생성된 호출스택에 run()을 올린다.
  • 호출스택의 첫번째 메서드가 main이 아닌 run 메서드이다.
  • 한 스레드에 예외가 발생해서 종료되어도 다른 스레드에 영향을 미치지 않는다.
  • 실행결과에 main 스레드의 호출스택이 없는 이유는 main 스레드가 종료되었기 때문이다.
<code />
/* Thread의 run()을 오버라이딩 */ public class ExtendsThread extends Thread { public void run() { throwException(); } public void throwException() { try { throw new Exception(); } catch (Exception e) { e.printStackTrace(); } } }
<code />
public class ThreadEx { public static void main(String[] args) { // Thread의 자손 클래스 인스턴스 생성 ExtendsThread t1 = new ExtendsThread(); t1.start(); } }

실행 결과

<code />
java.lang.Exception at thread.ExtendsThread.throwException(ExtendsThread.java:15) at thread.ExtendsThread.run(ExtendsThread.java:6)

 

3.2. run()

  • main 메서드에서 run()을 호출하는 것은 생성된 스레드를 실행시키는것이 아닌,
    다순 클래스에 선언된 메서드를 호출하는 것일 뿐이다.
  • 위의 예제와 달리 스레드가 새로 생성이 되지 않았고 그냥 run()이 실행되었을 뿐이다.
  • 호출스택에 main 메서드가 포함되어 있음
<code />
/* Thread의 run()을 오버라이딩 */ public class ExtendsThread extends Thread { public void run() { throwException(); } public void throwException() { try { throw new Exception(); } catch (Exception e) { e.printStackTrace(); } } }
<code />
public class ThreadEx { public static void main(String[] args) { // Thread의 자손 클래스 인스턴스 생성 ExtendsThread t1 = new ExtendsThread(); t1.run(); } }

실행 결과

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

4. 💡 Thread 이름 조회

스레드의참조값.getName()

<code />
Thread thread3 = new Thread(new Runnable() { public void run() { System.out.println("Get Thread Name"); } }); thread3.start(); System.out.println("thread3.getName() = " + thread3.getName());
<code />
Get Thread Name thread3.getName() = Thread-0

5. 💡 Thread 이름 설정

스레드의참조값.setName()

<code />
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());

6. 💡 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); 의 동작을 위해 형식적으로 사용한 문법요소임

 

<code />
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" : "") ); } } }
<code />
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 객체를 공유하게 된다.


7. 💡 Critical Section & Lock

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

 

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

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

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

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

8. 💡 Multi-Thread Programming

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

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

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

9. 💡 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

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

검색 태그