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);
- 스레드를 일시정지 시키는 메소드. ※ 스레드가 정지되면 대기열에서 대기중인 다른 스레드가 실행됨
- Thread.sleep()는 반드시 try .. catch문의 try 블럭내에 작성
- 간단하게 말하면, 스레드의 동작을 1초동안 멈추는 코드
- try { ... } catch ( ~ ) { ... }
- 예외처리에 사용되는 문법
- try 블록 내 코드를 실행하다 예외&에러 발생시 catch문 내의 코드 실행
- 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(경쟁 상태)에 들어가거나 같은 객체에 동시에 접근해도 연산결과의 정합성이 보장될 수 있게끔 메모리 가시성이 확보된 상태를 의미함
- java.util.concurrent 패키지 하위의 클래스 사용
- 인스턴스 변수를 두지 않음
- Singleton 패턴을 사용함 (이 때, 일반적으로 구현하는 Singleton Pattern은 Thread-Safe 하지 않음) (https://github.com/ksundong/TIL/blob/master/DesignPattern/singleton-pattern.md)
- 동기화 블럭에서 연산을 수행함
'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 |