본문 바로가기

JAVA

쓰레드

1. 프로세스와 쓰레드

프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 된다.
ex) 이클립스를 실행하면 필요한 메모리를 할당받아 이클립스는 프로그램에서 프로세스가 된다.

 

 

프로세스 (공장)

실행 중인 프로그램

프로그램을 수행하는 데 필요한 데이터 + 메모리(자원) + 쓰레드

쓰레드 (일꾼)
프로세스의 자원을 이용해서 실제로 작업을 수행하는 것

프로세스는 쓰레드로 이루어져 있다.

 

멀티 쓰레드 프로세스
둘 이상의 쓰레드를 가진 프로세스
(대부분의 프로세스는 멀티쓰레드로 이루어져 있음)

 

 

 

 

멀티 쓰레딩의 장점
- CPU 사용률 향상
- 자원을 효율적으로 사용

- 사용자에 대한 응답성 향상

- 작업이 분리되어 코드가 간결해짐

 

멀티 쓰레딩의 단점

- 여러 쓰레드가 같은 프로세스 내에서 자원을 공유하면서 작업을 하기 때문에 동기화, 교착상태, 기아와 같은 문제 발생 가능

 

교착상태(deadlock)

두 쓰레드가 자원을 점유한 상태에서 서로 상태 편이 점유한 자원을 사용하려고 기다리느라 진행이 멈춰있는 상태

ex) 망치와 삽을 가진 일꾼이 있다. 망치를 가진 사람은 삽이 필요하고, 삽을 가진 사람은 망치가 필요하다.

      서로 상대방의 것을 내놓으라고 버티느라 멈춰 있는 것. 이것을 교착상태라고 한다.

 

기아현상

특정 쓰레드는 작업할 기회도 얻지 못하고 죽어버림

 

 

2. 쓰레드의 구현과 실행

1. Thread 클래스 상속 (자바는 단일 상속이기 때문에, 이걸 사용하면 다른 상속을 하지 못하게 됨)

 

class MyThread extends Thread {
    public void run() {
    	// 작업 내용 오버라이딩
    }
}

 

2. Runnable 인터페이스 구현 (이걸 사용하자)

 

class MyThread implements Runnable {
    public void run() {
    	// 작업 내용 구현
    }
}

 

package java13;

public class Ex13_1 {
	public static void main(String[] args) {
		
		// 1. 상속한 경우 바로 사용 가능
		ThreadEx1_1 t1 = new ThreadEx1_1();
		
		// 2. 인터페이스를 구현한 경우
		Runnable r = new ThreadEx1_2();
		Thread t2 = new Thread(r); // Thread(Runnable target) 생성자를 이용
		//Thread t2 = new Thread(new ThreadEx1_2());
		
		t1.start();
		t2.start();
		
	}

}

class ThreadEx1_1 extends Thread {
	
	public void run() {
	
		for(int i = 0; i < 5; i++) {
			System.out.println(getName()); // getName(): 쓰레드의 이름 출력
		}
		
	}
}

class ThreadEx1_2 implements Runnable {

	@Override
	public void run() {
		
		for(int i = 0; i < 5; i++) {
			// Thread.currentThread() : 현재 실행중인 쓰레드를 반환
			System.out.println(Thread.currentThread().getName());
		}
		
	}
	
}
/*
Thread-0
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-0
Thread-0
Thread-0
Thread-0
*/


쓰레드의 실행 순서는 OS의 스케쥴러가 작성한 스케줄에 의해 결정된다. 즉, 순서대로 실행되는 것이 아니라 뒤죽박죽 실행된다.

자바는 OS에 독립적이긴 하지만, 실제로는 종속적인 부분도 있는데 쓰레드도 한 부분이라고 할 수 있다.

 

 

3. 싱글쓰레드와 멀티쓰레드

main 메서드 안에 모두 넣고 돌렸던 것이 싱글쓰레드. 여태까지 싱글쓰레드만 사용했다고 생각하면 된다.

 

싱글쓰레드는 한 작업을 마친 후, 다른 작업을 시작한다.

멀티쓰레드는 여러 개의 쓰레드를 번갈아 가면서 작업을 수행한다.

(컨텍스트 스위칭 : 프로세스 또는 쓰레드 간의 작업 전환)

따라서, 멀티쓰레드가 싱글쓰레드에 비해 작업이 조금 느린 경우가 있지만, 결과적으로는 더 높은 효율을 낸다.

 

멀티쓰레드가 시간이 더 걸리는데 왜 좋은가?

채팅할 때, 사진 전송하면서 메시지 못 보낸다고 생각해보자. 매우 불편하겠지? (이 경우, 싱글쓰레드)

하지만, 사진을 전송하면서 메시지도 보낸다고 생각해보자. 동시에 두 가지 작업 가능 (이 경우, 멀티쓰레드)

즉, 시간은 좀 더 걸릴지라도 동시에 여러 작업을 할 수 있으므로 효율성이 높아진다고 할 수 있다.

 

 

4. 쓰레드의 I/O 블락킹

I/O 블락킹이란 쓰레드가 입출력 처리를 위해 기다리는 것을 말한다. (입출력 대기로 인한 작업 중단)

 

두 쓰레드가 서로 다른 자원을 사용하는 경우에는 멀티쓰레드로 작업하는 것이 효율적이다.

예를 들어, 사용자로부터 데이터를 입력받는 작업, 네트워크로 파일을 주고받는 작업, 프린터로 파일을 출력하는 작업과 같이

외부기기와의 입출력을 필요로 하는 경우이다.

 

이런 경우 싱글쓰레드로 구현해버리면, 입력이나 출력이 끝날 때까지 무한정 기다려야만 한다. 시간 낭비, CPU 낭비가 되겠지?

 

 

싱글쓰레드를 이용한 입출력 예시
사용자가 입력을 할 때까지 무한정 기다리는 모습. 입력을 해야 다음 코드가 실행된다. -> 효율성이 매우 낮음

package java13;

import javax.swing.JOptionPane;

public class Ex13_4 {
	public static void main(String[] args) {
		
		String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
		System.out.println("입력하신 값은 " + input + "입니다.");
		
		for(int i = 10; i > 0; i--) {
			System.out.print(i + " ");
			try {
				Thread.sleep(1000); // 1초 시간을 지연한다.
			} catch (Exception e) {}
		}
		
	}

}
/*
입력하신 값은 qwer입니다.
10 9 8 7 6 5 4 3 2 1
*/

 

멀티쓰레드를 이용한 입출력 예시

일단 main 메서드를 실행하고, 사용자의 입력이 들어올 때까지 기다리지 않는다.

난(main 쓰레드) 일단 실행할 테니 입력하고 싶을 때 해~ CPU의 활용성 극대화. 매우 효율적!

 

package java13;

import javax.swing.JOptionPane;

public class Ex13_5 {
	public static void main(String[] args) {
		
		ThreadEx5_1 th1 = new ThreadEx5_1();
		th1.start();
		
		String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
		System.out.println("입력하신 값은 " + input + "입니다.");
		
	}

}

class ThreadEx5_1 extends Thread {
	
	public void run() {
		for(int i = 10; i > 0; i--) {
			System.out.print(i + " ");
			try {
				sleep(1000);
			} catch (Exception e) {}
		}
	}
}
/*
10 9 8 7 6 입력하신 값은 qwer입니다.
5 4 3 2 1
*/

 

5. 쓰레드의 우선순위

쓰레드가 수행하는 작업의 중요도에 따라 쓰레드의 우선순위를 부여하여 특정 쓰레드가 더 많은 작업 시간을 갖도록 할 수 있다.

ex) 파일 전송 기능이 있는 메신저의 경우, 파일 전송보다 채팅의 쓰레드가 우선순위가 더 높아야 함

      윈도우의 경우, 마우스 포인터의 우선순위가 높음

 

우선순위의 범위는 1~10이며, 기본 5로 셋팅되어 있다.

다만, 중요한 점은 우선순위를 지정해도 더 많은 실행 시간과 실행 기회를 갖게 되지만, 더 빨리 완료된다고 말할 수 없다.

OS의 스케쥴링에 따라 다르기 때문에, 기대만 할 뿐 100%라고 말할 수 없다.

 

package java13;

public class Ex13_6 {
	public static void main(String[] args) {
		
		ThreadEx6_1 th1 = new ThreadEx6_1();
		ThreadEx6_2 th2 = new ThreadEx6_2();
		
		// th2 쓰레드에 우선순위 부여 -> |를 찍는 쓰레드
		// start() 하기 전에 우선순위를 줘야함!!
		th2.setPriority(7); 
		
		System.out.println(th1.getPriority()); // 5 (아무 설정도 하지 않으면 기본 5)
		System.out.println(th2.getPriority()); // 7
		
		th1.start();
		th2.start();
		
	}

}

class ThreadEx6_1 extends Thread {
	public void run() {
		for(int i = 0; i < 100; i++) {
			System.out.print("-");
			for(int x = 0; x < 10000000; x++); // 시간 지연용
		}
	}
}

class ThreadEx6_2 extends Thread {
	public void run() {
		for(int i = 0; i < 100; i++) {
			System.out.print("|");
			for(int x = 0; x < 10000000; x++); // 시간 지연용
		}
	}
}

 

 

 

6. 데몬 쓰레드

다른 일반 쓰레드의 작업을 돕는 보조적인 역할

예를 들어, 가비지 컬렉터, 워드프로세서의 자동 저장, 화면 자동갱신이 있다.

 

데몬 쓰레드는 무한루프와 조건문을 이용해서 실행 후 대기하고 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성한다.

일반 쓰레드가 종료되면, 알아서 무한루프를 멈추고 종료된다. 따라서, 우리가 따로 무한루프를 멈춰줄 필요가 없다.

 

public void run() {
	while(true) { // 1.무한루프 (언제 일반쓰레드가 종료될지 모르므로)
            try { 
                Thread.sleep(3 * 1000); // 무한루프니까 쓰레드를 3초마다 쉬게 해주자
            } catch(InterruptedExcetion) {}

            if(autoSave) autoSave(); // 2.조건이 만족할 때 자동저장 실행
	}
}

 

package java13;

public class Ex13_7 implements Runnable {
	
	static boolean autoSave = false;
	
	public static void main(String[] args) {
		
		Thread t = new Thread(new Ex13_7()); // Thread(Runnable Target)
		
		// 클래스에 있는 쓰레드를 데몬 쓰레드로 설정하겠다!
		// 데몬 쓰레드 설정은 꼭 start() 전에 설정하도록 하자!
		t.setDaemon(true); 
		t.start();
		
		for(int i = 1; i <= 10; i++) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {}
            
			System.out.print(i + " ");
			if(i==5) autoSave = true; // 5초에 true로 바꿔줘.
		}
		
	}
	
	// 데몬 스레드
	public void run() {
		while(true) {
			try {
				// true로 바뀐 뒤로는 3초마다 실행해줘.
				// 3초마다 실행되지만, 결국 3초씩 쉬라는 의미이다.
				Thread.sleep(3 * 1000); 
			} catch (InterruptedException e) {}
			
			if(autoSave) autoSave();
		}
	}
	
	public void autoSave() {
		System.out.println("자동 저장 완료");
	}
}
/*
1 2 3 4 5 자동 저장 완료
6 7 8 자동 저장 완료
9 10
*/

 

 

7. 쓰레드의 상태

NEW 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태
RUNNABLE 실행 중 또는 실행 가능한 상태
BLOCKED 동기화블럭에 의해서 일시정지된 상태 (lock이 풀릴 때까지 기다리는 상태)
WAITING,
TIME_WATING
쓰레드의 작업이 종료되지는 않았지만 실행가능하지 않은 일시정지상태
TIME_WAITING은 일시정지시간이 지정된 경우를 의미
TERMINATED 쓰레드의 작업이 종료된 상태

 

 

 

 

8. 쓰레드의 실행 제어

쓰레드의 스케줄링과 관련된 메서드

* static 메서드는 쓰레드 자기 자신에게만 호출 가능하다. 즉, 다른 쓰레드에 적용 불가하다는 뜻.

  다른 쓰레드한테 억지로 "자라(sleep), 양보해라(yield) 불가!"

 

* resume(), stop(), suspend()는 교착상태를 유발시켜 사용하지 않는 것이 권장됨

 

static void sleep(long millis)
static void sleep(long millis, int nanos)
지정된 시간(천분의 일초 단위)동안 쓰레드를 일시정지시킴.
지정한 시간이 지나고 나면, 자동적으로 다시 실행대기상태
예외 처리가 필수임!
void join()
void join(long millis)
void join(long millis, int nanos)
쓰레드 자신이 하던 작업을 잠시 멈추고 다른 쓰레드의 작업을 기다림
예외 처리가 필수임!
void interrupt() <-> sleep(), join() sleep()이나 join()에 의해 일시정지상태인 쓰레드를 깨워서 실행대기상태로 만든다.
해당 쓰레드에서는 Interrupted Exception이 발생함으로써 일시정지 상태를 벗어나게 한다.
void stop() 쓰레드를 즉시 종료시킨다.
void suspend() <-> resume() 쓰레드를 일시정지시킨다. 
void resume() <-> suspend() 일시정지 상태에 있는 쓰레드를 실행대기상태로 만든다.
static void yield() 자신에게 주어진 실행 시간을 다른 쓰레드에게 양보하고 자신은 실행대기상태가 된다.

 

 

9. 쓰레드의 동기화

동기화를 하는 이유?
멀티쓰레드 프로세스의 경우
여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업하기 때문에 서로의 작업에 영향을 준다.

따라서, 한 쓰레드가 특정 작업을 끝마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 하는 작업이 필요한데 이것이 동기화이다.


다른 쓰레드가 간섭하지 못하게 막는 것

간섭하면 안되는 문장을 임계 영역으로 만들고

임계 영역은 자물쇠를 얻은 하나의 쓰레드만 출입이 가능하다

 

*객체 1개에 락 1개 -> 한 번에 하나의 쓰레드만 출입이 가능하다는 뜻

*임계 영역이 많을수록 성능이 떨어지게 된다. 따라서 영역이나 개수를 최소화하는 것이 좋다.


public class Ex13_12 {
	public static void main(String[] args) {
		Runnable r = new RunnableEx12(); // 쓰레드 생성
		// 동기화 테스트를 위해 두 개의 쓰레드 돌리기
		new Thread(r).start();
		new Thread(r).start();
	}
}

class Account {
	private int balance = 1000; // 동기화를 위해서는 변수를 꼭 private으로 선언!
	
	public int getBalance() {
		return balance;
	}
	
	// 메서드를 동기화
	public synchronized void withdraw(int money) {
		if(balance >= money) { // 잔액이 남아 있을 경우에만 출금 가능
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {}
			balance = balance - money;
		}
	}
}

class RunnableEx12 implements Runnable { // 쓰레드 구현

	Account acc = new Account(); // 쓰레드는 계좌를 가짐
	
	@Override
	public void run() {
		while(acc.getBalance() > 0) { // 잔액이 남아 있을 경우에만 출금 가능
			int money = (int)(Math.random() * 3 + 1) * 100; // 100, 200, 300 중 랜덤으로
			acc.withdraw(money);
			System.out.println("잔액: " + acc.getBalance());
		}
	}	
	
}

/* 동기화 전
잔액: 600
잔액: 600
잔액: 300
잔액: 0
잔액: -200
*/

/* 동기화 후
잔액: 900
잔액: 800
잔액: 700
잔액: 400
잔액: 300
잔액: 0
잔액: 0
*/

 

동기화 하기 전에 실행 결과가 음수가 나오는 이유는

main 메서드에서 생성한 하나의 쓰레드가 if문의 조건식을 통과하고 출금하기 바로 직전에

또 다른 쓰레드가 끼어들어서 출금을 먼저 했기 때문이다. 두 개의 쓰레드 충돌이 발생하면서 꼬였다고 할 수 있다.

이를 해결하기 위해 출금 메서드에 synchronized 키워드를 붙여 동기화 처리를 해주었다.

 

또 한 가지 기억해야 하는 부분은 동기화를 할 때 사용하게 되는 인스턴스 변수는 private로 선언하여 직접 접근을 막아야한다는 것이다.

balance를 private이 아닌 public으로 선언하면,아무리 동기화를 해놔도 외부에서 이 값을 마음대로 바꿀 수 있기 때문이다!

 

 

10. 쓰레드의 동기화 - wait(), notify()

 

'JAVA' 카테고리의 다른 글

7장_2  (0) 2021.01.22
7장_1  (0) 2021.01.22
다형성 / 동적 바인딩 / 인터페이스 / 추상클래스  (0) 2021.01.18
static은 언제 붙일까?  (0) 2021.01.14
Comparator / Comparable 인터페이스  (0) 2021.01.12