개발지식 먹는 하마 님의 블로그
Java - 스레드(Thread) 본문
[ Thread ]
우리는 컴퓨터에서 여러 개의 작업을 동시에 진행하는 것이 가능하다.
한 개의 CPU가 작업을 잘게 쪼개어 모든 작업을 차례로 짧은 시간 동안 실행한다.
CPU가 여러 개이거나 하나의 CPU에 여러 개의 코어가 있다면,
각각의 CPU 또는 코어가 하나씩 작업을 진행할 수 있으므로 같은 시간대에 여러 개의 작업이 병렬적으로 실행될 수 있다.
이때, 잘게 쪼개어지는 작업을 스레드(Thread)라고 한다.
하나의 프로세스 내에 여러 개의 쓰레드가 존재할 수 있고, 스레드들이 동시에 실행될 수 있다.
이를 멀티쓰레딩이라고 한다.
< Thread는 직원, GPU는 대기업... >
나는 GPU기반 CUDA를 먼저 학습하고 사용했기 때문에 병렬프로그래밍에 익숙한 편이다.
병렬프로그래밍을 공부하는 초기에 thread를 직원에 비유해서 이해했다.
thread를 직원이라고 할 때,
1인 회사는 thread가 하나밖에 없다.
그러니까 회사 프로젝트 진행, 회계 관리 등의 업무를 혼자 다 해야 한다.
스케줄에 지장이 가지 않을 정도로 분량을 쪼개고 순서를 정해서 혼자 처리하는 것이다.
멀티코어나 여러 개의 CPU는 기업이다.
직원이 어느 정도 있다.
1인 회사가 혼자 해야 하던 일을 여러 명이 나눠서 하는 것이다. (멀티스레딩)
당연히 많은 양을 더 빨리 처리할 수 있다.
그런 의미에서 GPU는 대기업이라고 할 수 있겠다.
(비싼 가격에는 그만한 이유가 있는...)
< Thread 작성 방법 >
자바에서 Thread를 생성하는 방법은 2가지이다.
- Thread 클래스를 상속받는 방법
- Runnable 인터페이스를 구현하는 방법
1번은 간단하고 2번은 유용하다.
1. Thread 클래스 상속
- Thread의 서브 클래스 정의
- run 메서드 오버라이드 (Thread가 실행할 작업 내용 입력)
- Thread를 상속받으면 다른 클래스를 상속할 수 없게 된다. (당연한 이야기, 단일 상속이니까)
public class MyThread extends Thread {
private Strng threadName;
public MyThread(String name){
threadName = name;
}
public void run(){
for(int i = 0; i < 10; i++){
try{
Thread.sleep(10); //Thread를 10ms 대기시킴
}
catch(InterruptedException e){ //sleep으로 인해 발생할 수 있는 예외
e.printStackTrace();
}
}
}
}
Thread t1 = new MyThread("thread1"); //Thread 객체 생성
t1.start(); //thread를 실행시킨다.
반드시 start를 통해서 스레드를 시작시켜야 한다.
run을 통해 실행시키면 병렬프로그래밍이 되지 않는다.
2. Runnable 인터페이스 구현
- Runnable 인터페이스를 상속한다.
- 클래스 내에서 run 메서드를 구현한다.
public class MyThread implements Runnable{ //Runnable 인터페이스 상속
private String threadName;
public MyThread(String name){
threadName = name;
}
public void run(){ //run 메소드 재정의
//Thread가 할 일
}
}
Thread t1 = new Thread(new MyThread("thread1"));
t1.start();
Thread 메소드 | 설명 |
run() | 스레드가 실행할 작업을 정의. 반환 값이 없다. |
start() | Runnable 객체를 새 스레드로 실행 |
join() | 해당 메소드를 호출한 스레드 종료까지 메인 스레드가 대기 |
sleep(ms) | 스레드를 일정 시간 (밀리초) 동안 일시 정지 |
< Thread의 동기화 (Synchronization) >
동기화는 한 번에 하나의 스레드만이 임계 영역을 실행하도록 제어하는 방법이다.
하나의 스레드가 공유 자원에 대한 조작을 끝낸 다음, 다른 쓰레드가 해당 자원에 접근할 수 있게 된다.
* 임계 (코드) 영역 (Critical code section)
쓰레드 간의 실행 순서에 따라 결과가 달라질 수 있는 영역
공유 자원을 조작하는 코드 부분
동시에 실행되는 여러 개의 쓰레드가 동일한 공유 자원을 접근할 때, 문제가 생길 수 있다.
> 문제가 생기는 예시
예를 들어, 잔액이 10000원 일 때, Thread1이 5000원을 입금하고 Thread2가 3000원을 출금한다고 하자.
우리가 원하는 건, Thread1에서 저장된 결과를 Thread가 사용하는 것이다.
Thread1 : 10000 + 5000 = 15000
Thread2 : 10000 - 3000 = 7000
그러나 동시에 같은 자원에 접근했기 때문에 둘 다 10000원을 기준으로 연산을 하고,
Thread1이 15000원을 저장했지만 이후 Thread2가 7000원을 저장했기 때문에 잔액이 7000원으로 출력되게 된다.
이런 문제를 해결하는 방법이 바로 동기화이다.
메서드 전체 또는 일부 코드에 대해서 동기화할 수 있다.
public synchronized void test(){}//메소드 전체를 동기화
public void test2(){
synchronized(sync_object){ //일부 코드만 동기화
//공유 자원에 접근하는 코드
}
//그 외의 코드
}
> 동기화 객체 ( syncObject )
일부 코드에 대해서 동기화 할 때는 sync_object가 필요하다.
sync_object는 동기화 객체 또는 락 객체라고 한다.
- 동기화를 위한 기준점 역할을 한다.
- 한 시점에 한 개의 스레드만 이 객체에 대한 락을 획득할 수 있다.
- 객체의 타입에 제한이 없다. (기본타입 int, double 등 제외)
- null 절대 불가
- 중간에 객체 타입이 변해선 안된다.
- 동일한 리소스에 대해서 동일한 동기화 객체를 사용해야 한다.
(서로 다른 동기화 객체 사용 시, 동기화 효과가 사라진다.)
동기화 객체의 타입에 제한이 없기 때문에 다양한 방법으로 객체를 사용할 수 있다.
- 전용 동기화 객체
- this : 현재 인스턴스 전체에 대한 동기화가 필요할 때 사용, 외부 간섭 주의
- 클래스
- 필드 : 특정 필드에 대한 동기화가 필요할 때, 단 필드 변경 시 주의 필요
//전용 동기화 객체
private final Object lock = new Object();
//this
synchronized(this) { ... }
//클래스 객체
synchronized(MyClass.class) { ... }
//필드
private final List<String> list = new ArrayList<>();
synchronized(list) { ... }
> 멀티스레딩과 동기화 예제
public class Account implements Runnable { //Runnable 상속
private int balance = 10000;
public synchronized void deposit(int amount) { //임계 영역 동기화
int cur_dep = balance;
cur_dep += amount;
balance = cur_dep;
}
public int getBalance() {
return balance;
}
@Override
public void run() {
// 스레드 당 5번 입금 수행
for (int i = 0; i < 5; i++) {
deposit(1000);
System.out.println(Thread.currentThread().getName()
+ " 입금 후 잔액: " + getBalance());
}
}
public static void main(String[] args) {
Account account = new Account();
// 두 스레드가 동일한 Account 객체 공유
Thread t1 = new Thread(account, "thread1");
Thread t2 = new Thread(account, "thread2");
t1.start();
t2.start();
// 메인 스레드 대기
try {
t1.join(); //join는 InterruptedException 발생시킬 가능성이 있음
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("최종 잔액: " + account.getBalance());
}
}
< 생산자 - 소비자 관계 >
생산자-소비자 관계는 일정 크기의 저장소에 대해서,
한 스레드는 데이터를 저장하고 다른 스레드는 저장된 데이터를 쓰는 관계를 말할 수 있다.
데이터를 저장하는 스레드는 용량이 다 차면 생산을 멈췄다가 빈 공간이 생기면 다시 생산을 시작한다.
저장된 데이터를 쓰는 스레드는 쓸 데이터가 없을 때는 소비를 멈췄다가 데이터가 생기면 다시 소비를 시작한다.
이런 상황을 처리하기 위해서 Object에서 제공하는 wait와 notify 메소드를 사용할 수 있다.
Object에서 제공하는 메소드 | 설명 |
wait() | Thread가 일시 정지 상태에 들어가게 함 |
notify() | Thread를 깨움 |
public synchronized void get(){
while(isEmpty){
try{
wait(); //Thread를 일시 정지 시킴
} catch(InterruptedException e){
}
}
notify(); //Thread 깨우기
}
< 동기화 남발 주의! >
동기화를 남발하면 여러 개의 Thread를 사용해도 병렬프로그래밍이라고 볼 수 없게 된다.
(직원이 여러 명인데 줄 서서 한 명씩 일처리 하는 것과 동일하다.)
따라서, 임계 영역과 같이 필요한 최소의 영역만 동기화하는 것이 바람직하다.
'Java' 카테고리의 다른 글
JAVA의 구조 (0) | 2025.02.24 |
---|---|
Java - 네트워크 프로그래밍 (0) | 2025.02.09 |
Java - 배열과 리스트 (1) | 2025.02.06 |
Java - 인터페이스 (0) | 2025.02.05 |
Java - 오버로딩, 오버라이딩 (1) | 2025.02.04 |