이지은님의 블로그
250106 - Java 다중 작업 처리 : 프로세스와 스레드의 개념 및 활용 본문
▷ 코드 문제풀이
[JAVA] 코드카타 - (1)~(5)
문제 (1) : 두 수의 차정수 num1과 num2가 주어질 때, num1에서 num2를 뺀 값을 return하도록 soltuion 함수를 완성해주세요.https://school.programmers.co.kr/learn/courses/30/lessons/120803 프로그래머스SW개발자를 위
queenriwon3.tistory.com
[JAVA] 코드카타 - (6)~(10)
문제 (6) : 두 수의 합정수 num1과 num2가 주어질 때, num1과 num2의 합을 return하도록 soltuion 함수를 완성해주세요.https://school.programmers.co.kr/learn/courses/30/lessons/120802 프로그래머스SW개발자를 위한 평
queenriwon3.tistory.com
▷ 오늘 배운 것
오늘은 java 5주차 내용정리를 전부 정리하고 내일부터 계산기 과제 구현내용을 작성해보려고 한다.
1. 프로세스
2. 스레드
1) 싱글 스레드
2) 멀티 스레드
3) 멀티 스레드의 구현방법
4) 우선 순위에 따른 스레드 분류
5) 스레드 그룹
6) 쓰레드의 상태와 제어
1. 프로세스
프로세스는 “실행 중인 프로그램”을 의미한다.
OS가 프로그램 실행을 위한 프로세스를 할당해줄 때 프로세스 안에 프로그램 Code와 Data 그리고 메모리 공간(Stack, Heap)을 함께 할당
Code | Java main메소드 같은 코드 |
Data | 프로그램이 실행 중 저장할 수 있는 저장공간(전역변수, 정적변수(static), 배열 등 초기화된 데이터를 저장하는 공간) |
Memory | - Stack : 지역변수, 매개변수 리턴 변수를 저장하는 공간 - Heap : 프로그램이 동적으로 필요한 변수를 저장하는 공간 (new()) |
2. 쓰레드(Thread)
프로세스가 작업 중인 프로그램에서 실행 요청이 들어오면 쓰레드를 만들어 명령을 처리 할 수 있다.
이때 각 쓰레드는 Stack영역을 할당받고 메모리공간 Heap을 공유한다.
JVM(프로세스) —> main스레드는 자바프로그램을 실행시킬때 JVM에 의해 실행됨
1) 싱글스레드
- 프로세스 안에서 하나의 스레드만 실행되는 것
- Java 프로그램의 경우 main() 메서드만 실행(메인스레드)
- JVM의 메인 쓰레드가 종료되면, JVM도 같이 종료.
2) 멀티스레드
멀티스레드의 장점
- 여러개의 작업을 동시에 할 수 있음.
- 메모리를 공유하기때문에 효율적사용.
- 응답쓰레드와 작업쓰레드를 분리하여 응답
멀티쓰레드의 한계
- 동기화문제가 발생할 수 있음-> 자원을 서로 사용하려고 하는 충돌
- 교착상태(각 스레드가 서로의 리소스를 내놓지않고 무한 대기)가 발생할 수 있음
- 멀티스레드는 운영체제의 상황에따라 스레드의 결과값이 다름 (예측불가)
3) 멀티스레드의 구현방법
- 구현 방법(1) - Thread클래스 상속
public class Main {
public static void main(String[] args) {
TestThread thread = new TestThread();
thread.start();
}
}
class TestThread extends Thread {
@Override
public void run() {
for (int i = 0; i <100; i++) {
System.out.print("*");
}
}
}
Thread 클래스를 상속받은 클래스(extends Thread)에 run()메소드를 오버라이딩하여 스레드로 실행 할 내용 작성한다.
클래스이름 인스턴스이름 = new 클래스이름()으로 객체를 생성한 뒤 인스턴스이름.start()으로 실행시킨다.
(메소드는 run()으로 명명했으나 실행은 start()로 실행)
- 구현 방법(2) - Runnable 인터페이스 상속(확장성에 유리함)
Java는 다중상속을 지원하지 않기때문에 Thread(1번방법)은 확장성이 떨어짐
-> Runnable은 인터페이스기 때문에 다른 필요한 클래스를 상속받을 수 있음.(확장성에 유리)
public class Main {
public static void main(String[] args) {
Runnable run = new TestRunnable();
Thread thread = new Thread(run);
thread.start();
}
}
class TestRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i <100; i++) {
System.out.print("$");
}
}
}
Runnable 인터페이스를 상속받은 클래스(implements Runnable)에 run()메서드를 오버라이딩하여 스레드로 실행할 내용 작성한다.
이후 Runnable 인스턴스이름 = new 클래스이름();으로 객체를 생성
Thread 인스턴스이름 = new Thread(Runnable인스턴스이름); 으로 스레드를 만든다.
(Thread는 static형으로 호출없이 사용할 수 있음)
스레드 인스턴스.start();로 실행시킴
- 구현 방법(3) - 람다식(Runnable을 클래스가 아닌 람다로 생성)
Runnable task = () -> {
int sum = 0;
for (int i = 0; i < 50; i++) {
sum += i;
System.out.println(sum);
}
System.out.println(
Thread.currentThread().getName() + " 최종 합 : " + sum);
};
Thread thread1 = new Thread(task);
thread1.setName("thread1");
Thread thread2 = new Thread(task);
thread2.setName("thread2");
thread1.start();
thread2.start();
람다식을 이용해서 Runnable형태의 스레드를 지정한다.
Thread 인스턴스이름 = new Thread(Runnable인스턴스이름); 으로 스레드를 만든다.
스레드 인스턴스.start();로 실행시킬 수 있다.
기타 사용 할 수 있는 메소드
thread1.setName("thread1"); // 스레드의 이름 지정
Thread.currentThread().getName() // 현재 진행하고 있는 스레드의 이름(문자열)
4) 우선 순위에 따른 스레드 분류
👉 데몬 쓰레드
보이지 않는 곳에서 실행되는 낮은 우선순위를 가진 쓰레드
우선순위가 낮음 = 상대적으로 다른 쓰레드에 비해 리소스를 적게할당받는다.
—> 데몬 쓰레드는 우선순위가 낮고 다른 쓰레드가 모두 종료되면 강제 종료당한다.
Thread.setDaemon(true); // 데몬쓰레드를 설정
👉 사용자 쓰레드
보이는 곳에서 실행되는 높은 우선순위를 가진 쓰레드
프로그램 기능을 담당하며 대표적으로 메인쓰레드. 데몬이 아니면 다 사용자 쓰레드
사용자 쓰레드의 작업이 끝나면 데못쓰레드도 자동으로 종료시킨다.
쓰레드 우선순위
- 쓰레드 작업의 중요도에 따라서 쓰레드의 우선순위를 부여 가능하다.
- 우선순위를 높게 지정할수록 더 많은 작업시간을 부여받아 빠른 처리를 할 수 있다.
- 쓰레드는 생성될때마다 우선순위가 정해지는데 사용자가 조절가능하다.
최대 우선순위 (MAX_PRIORITY) = 10
최소 우선순위 (MIN_PRIORITY) = 1
보통 우선순위 (NROM_PRIORITY) = 5
더 자세하게 나눈다면 1~10 사이의 숫자로 지정 가능
빠른 작업의 확률이 높을 뿐, 우선순위가 높다고 반드시 쓰레드가 먼저 종료되는 것은 아님
thread1.setPriority(8); // thread1의 우선 순위 8 지정
thread1.getPriority() // 우선순위 반환(int형)
5) 쓰레드 그룹
- 관련이 있는 쓰레드를 한 그룹으로 관리할 수 있다.
- 모든 쓰레드는 반드시 하나의 그룹에 포함되어있어야한다. Main 쓰레드는 system.main에 포함된다.
- (기본적으로 system.main에 포함되며 모든 쓰레드 그룹은 system그룹에 포함됨)
- 그룹으로 묶으면 동시중단이 가능하다.
// 쓰레드 그룹 생성
ThreadGroup group1 = new ThreadGroup("Group1"); // 쓰레드 그룹 클래스로 그룹생성
Thread thread1 = new Thread(group1, task, "Thread 1"); // 쓰레드를 생성할때 그룹, 쓰레드, 쓰레드 이름을 입력(오버로딩)
thread1.getThreadGroup().getName() // 쓰레드 그룹이름 확인가능
group1.interrupt(); // 해당 쓰레드 그룹 실행대기
5-1) 스레드 그룹 예제 코드 분석
public class Main {
public static void main(String[] args) {
Runnable task = () -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
break;
}
}
System.out.println(Thread.currentThread().getName() + " Interrupted");
};
// ThreadGroup 클래스로 객체를 만듭니다.
ThreadGroup group1 = new ThreadGroup("Group1");
// Thread 객체 생성시 첫번째 매개변수로 넣어줍니다.
// Thread(ThreadGroup group, Runnable target, String name)
Thread thread1 = new Thread(group1, task, "Thread 1");
Thread thread2 = new Thread(group1, task, "Thread 2");
// Thread에 ThreadGroup 이 할당된것을 확인할 수 있습니다.
System.out.println("Group of thread1 : " + thread1.getThreadGroup().getName());
System.out.println("Group of thread2 : " + thread2.getThreadGroup().getName());
thread1.start();
thread2.start();
try {
// 현재 쓰레드를 지정된 시간동안 멈추게 합니다.
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// interrupt()는 일시정지 상태인 쓰레드를 실행대기 상태로 만듭니다.
group1.interrupt();
}
}
사용된 핵심 코드
!Thread.currentThread().isInterrupted() // 해당 스레드가 일시중단되지 않을 때
catch (InterruptedException e) { break; }
System.out.println(Thread.currentThread().getName() + " Interrupted");
// 일시 중단되면 반복문을 빠져나옴 --> 스레드 일시중단 출력
InterruptedException
주로 스레드 작업 중 대기(wait), 슬립(sleep) 또는 다른 작업에서 인터럽트(interrupt)가 발생했을 때 발생.(sleep이 깨지면 실행대기 상태가 되면서 인터럽트 발생)
예를 들어, 다음과 같은 메서드가 호출 중 Thread.interrupt()로 중단되면 이 예외가 발생한다.
- Thread.sleep(milliseconds)
- Object.wait()
- Thread.join()
e.printStackTrace();
예외가 발생한 전체 스택 트레이스(에러가 발생한 위치를 추적하는 로그)를 출력
실제코드에서는 예외를 로깅하거나 적절한 처리를 추가하는 것이 좋음
Thread.sleep(ms)을 사용할때 기본적으로 try-catch를 해줘야함(위험감지)
6) 쓰레드의 상태와 제어
객체 생성 | NEW | 쓰레드 객체 생성, 아직 start() 메서드 호출 전의 상태 |
실행대기 | RUNNABLE | 실행 상태로 언제든지 갈 수 있는 상태 |
일시정지 | WAITING | 다른 쓰레드가 통지(notify) 할 때까지 기다리는 상태 |
일시정지 | TIMED_WAITING | 주어진 시간 동안 기다리는 상태 |
일시정지 | BLOCKED | 사용하고자 하는 객체의 Lock이 풀릴 때까지 기다리는 상태 |
종료 | TERMINATED | 쓰레드의 작업이 종료된 상태 |
- thread.start() : 쓰레드 객체를 실행 대기로 만듦(new-> runnable)
run(): 실행과 대기 -> 종료하되면 실행이 멈추게됨
- Thread.sleep(ms): 쓰레드를 지정된 시간 동안 멈추게 함 (TIMED_WAITING) 실행상태를 일시정지로 만들어 준다.
예외처리가 필수(interrupt()를 만나면 다시 실행되기 때문에 InterruptedException이 발생할 수 있음 -> try-catch으로 감싸야됨)
특정 쓰레드를 지목해서 멈추게 하는 것은 불가(static메서드) -> 스레드객체.sleep(ms)는 불가능
쓰레드의 일시정지:
일시정지 상태는 쓰레드가 실행할수 없는 상태
쓰레드가 다시 실행으로 넘어가기 위해서는 일시정지상태-> 실행대기상태 가 되어야됨
- thread.interrupt(): 일시정지 상태인 쓰레드를 실행대기 상태로 만듦.
sleep과 충돌하기 때문에 sleep상태에서 interrupt하게되면 예외를 발생시킴 -> catch에서 예외처리를 함
thread.isInterrupted() //쓰레드가 인터럽트 되었는지 참거짓으로 출력
while (!Thread.currentThread().isInterrupted())
// 현재 스레드가 인터럽트 되지 않을때까지 반복
// --> catch에서 break하여 빠져나오면 sleep상태여도 예외가 출력되지 않음
// thread.interrupt() 사용코드
Runnable task = () -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
};
Thread thread = new Thread(task, "Thread");
thread.start();
thread.interrupt(); // thread 스레드를 실행 대기상태로 만듦
System.out.println("thread.isInterrupted() = " + thread.isInterrupted());
- thread.join(ms):
정해진 시간동안 스레드가 작업하는 것을 기다림(시간을 지정하지 않았을때 지정한 쓰레드의 작업이 끝날때까지 기다림)
sleep과 마찬가지로 예외처리를 해야함(interrupt()를 만나면 기다리는 것을 멈추기 때문에 InterruptedException이 발생)
메인스레드에 thread.join()을 작성하면(시간을 지정하지 않으면) thread 스레드가 작업을 끝낼때까지 기다림
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
- thread.yield():
남은 시간을 다음쓰레드에게 양보하고 쓰레드 자신은 실행 대기 상태가됨 -> 다른 스레드에게 남은시간과 리소스 양보
interupt가 발생하면 catch에서 예외처리와 함께 다른 스레드에게 리소스를 양보한다.
try {
...
} catch (InterruptedException e) {
Thread.yield();
}
- synchronized(): 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 침범하지 못하도록 임계영역을 지정
임계 영역에는 Lock을 가진 단 하나의 쓰레드만 출입이 가능
// 방법1
public synchronized void asyncSum() {
...침범을 막아야하는 코드...
}
// 방법2
synchronized(해당 객체의 참조변수) {
...침범을 막아야하는 코드...
}
// synchronized() 예제코드
public class Main {
public static void main(String[] args) {
AppleStore appleStore = new AppleStore();
Runnable task = () -> {
while (appleStore.getStoredApple() > 0) {
appleStore.eatApple();
System.out.println("남은 사과의 수 = " + appleStore.getStoredApple());
}
};
for (int i = 0; i < 3; i++) {
new Thread(task).start();
} // 3개의 스레드를 한번에 생성후 실행
}
}
class AppleStore {
private int storedApple = 10;
public int getStoredApple() {
return storedApple;
}
public void eatApple() {
synchronized (this) {
if(storedApple > 0) {
try {
} catch (InterruptedException e) {
e.printStackTrace();
}
storedApple -= 1;
}
}
}
}
- wait(): 실행중이던 스레드가 Lock을 반납하고 기다림 -> 다른 스레드도 Lock을 얻어 실행 가능
- notify(): 작업을 진행할상황이 되면 다시 Lock을 얻을 수 있음(통지)
public void restock(String item) {
synchronized (this) {
while (inventory.size() >= Main.MAX_ITEM) {
System.out.println(Thread.currentThread().getName() + " Waiting!");
try {
wait(); // 재고가 꽉 차있어서 재입고하지 않고 기다리는 중!
Thread.sleep(333);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 재입고
inventory.add(item);
notify(); // 재입고 되었음을 고객에게 알려주기
System.out.println("Inventory 현황: " + inventory.toString());
}
}
- Condition:
Waiting pool내에 대기 중인 스레드를 구분하기 위해 wait() & notify() 대신 Condition의 await() & signal() 을 사용
waiting pool 내의 스레드를 분리하여 특정 조건이 만족될 때만 깨우도록 할 수 있으며, ReentrantLock 클래스와 함께 사용
특정조건(lock)에 맞는 await와 signal을 호출
public class Main {
public static final int MAX_TASK = 5;
private ReentrantLock lock = new ReentrantLock();
// lock으로 condition 생성
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private ArrayList<String> tasks = new ArrayList<>();
// 작업 메서드
public void addMethod(String task) {
lock.lock(); // 임계영역 시작
try {
while(tasks.size() >= MAX_TASK) {
String name = Thread.currentThread().getName();
System.out.println(name+" is waiting.");
try {
condition1.await(); // wait(); condition1 쓰레드를 기다리게 합니다.
Thread.sleep(500);
} catch(InterruptedException e) {}
}
tasks.add(task);
condition2.signal(); // notify(); 기다리고 있는 condition2를 깨워줍니다.
System.out.println("Tasks:" + tasks.toString());
} finally {
lock.unlock(); // 임계영역 끝
}
}
}
- Lock:
synchronized 블럭으로 동기화하면 자동적으로 Lock이 걸리고 풀리지만, 같은 메서드 내에서만 Lock을 걸 수 있다는 제약
- ReentrantLock
: 재진입이 가능. 특정조건에서 락을 풀고, 나중에 다시 락을 얻음
ReentrantLock을 사용하면, 같은 스레드가 이미 락을 가지고 있더라도 락을 유지하며 계속 실행할 수 있기 때문에 데드락이 발생하지 않음(유연성 증가)
public class MyClass {
private Object lock1 = new Object();
private Object lock2 = new Object();
public void methodA() {
synchronized (lock1) {
methodB();
}
} // lock1을 가지고 메소드B를 실행
// methodA에서 이미 lock1을 가지고 있으므로 lock2를 기다리는 상태가 되어 데드락발생
public void methodB() {
synchronized (lock2) {
// do something
methodA();
}
} // lock2을 가지고 메소드A를 실행
}
- ReentrantReadWriteLock
: 읽기를 위한 Lock(공유적)과 쓰기를 위한Lock(베타적)제공
읽기 Lock이 걸려있으면 다른 쓰레드들도 읽기 Lock을 중복으로 걸고 읽기를 수행가능(read-only)
읽기 Lock이 걸려있는 상태에서 쓰기 Lock을 거는 것은 허용되지 않습니다. (데이터 변경 방지)
- StampedLock
: ReentrantReadWriteLock + 낙관적인 lock기능(데이터를 변경하기 전에 락을 걸지 않는 것 -> 충돌가능성 줄어듬)
쓰기 작업이 빈번하지 않은 경우, 읽기 쓰기가 빠르게 처리됨.
쓰기 작업이 발생했을 때 데이터가 이미 변경된 경우 다시 읽기 작업을 수행하여 새로운 값을 읽어들이고, 변경 작업을 다시 수행
낙관적인 읽기 Lock은 쓰기 Lock에 의해 바로 해제 가능
무조건 읽기 Lock을 걸지 않고, 쓰기와 읽기가 충돌할 때만 쓰기 후 읽기 Lock을 건다.
▷ 앞으로
스레드를 잘 사용할 수 있도록 많은 연습을 해야할 것 같다. 특히 후반부는 어려운 내용이라 개념만 익히고 빨리 넘어간 면이 있어 나중에 시간이 되는대로 더 공부 해야할 것 같다고 느꼈다.
'TIL' 카테고리의 다른 글
250108 - Java 계산기 Lv.2 및 Lv.3 구현 트러블슈팅: NaN 처리, 간접 접근, Switch 개선, 입력 문제 해결 (0) | 2025.01.08 |
---|---|
250107 - Java 람다와 스트림, null 처리, Enum, StringBuilder 활용 및 데이터 변환 메소드 비교(valueOf(), parseInt()) (1) | 2025.01.07 |
250105 - Java 제네릭과 정규 표현식의 활용 (0) | 2025.01.05 |
250103 - Java 예외 처리 개념 및 과제 코드분석(오류와 예외, throws, throw, try-catch) (1) | 2025.01.03 |
250102 - Java 객체, 상속, 추상 클래스, 인터페이스의 이해 (0) | 2025.01.02 |