목표
자바의 멀티쓰레드 프로그래밍에 대해 학습하세요.
학습할 것 (필수)
- Thread 클래스와 Runnable 인터페이스
- 쓰레드의 상태
- 쓰레드의 우선순위
- Main 쓰레드
- 동기화
- 데드락
Thread 클래스와 Runnable 인터페이스
프로세스와 스레드에 대해 알아보자
프로세스(Process)
운영체제에서 실행중인 하나의 애플리케이션을 프로세스라고 한다
프로세스는 운영체제로부터 할당받은 데이터와 메모리 등의 자원과 쓰레드로 실행된다
하나의 프로그램을 여러개 실행하면 멀티 프로세스라고 한다
멀티 프로세스는 누구나 자주 사용한다. 간단한 예시로는 크롬을 여러번 실행하는 경우가 있다.
쓰레드(Thread)
프로세스가 가진 자원으로 작업을 하는 주체이다
프로세스는 하나 이상의 쓰레드를 가지며
두 개 이상의 쓰레드를 가진 프로세스를 멀티 쓰레드 프로세스라고 한다
쓰레드 생성과 실행 방법
생성은 2가지로 나눠진다
- Runnable 인터페이스 사용
- Thread 클래스 사용 (Runnable 인터페이스 구현체다)
// Runnable 인터페이스
class TestRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("r");
}
}
}
// Thread 클래스 상속
class TestThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("T");
}
}
}
실행방법은 둘 다 동일하다
start() 를 호출하면 된다
단, Runnable 구현체는 Thread 에 인스턴스를 넘겨주고 실행하자
public static void main(String[] args) {
// 쓰레드 구현 및 실행 방법
// 1. Runnable 인터페이스 사용
TestRunnable r = new TestRunnable();
Thread runnableSample = new Thread(r);
// 2. Thread 클래스 사용
TestThread threadSample = new TestThread();
// start 로 실행한다
runnableSample.start();
threadSample.start();
}
일반적인 흐름이라면 r 부터 100개 찍히고 T 가 실행되어야하는데 중간에 실행된 결과를 볼 수 있다.
여기서 start() 와 run() 의 차이점을 알아보자
run() 은 실행될 쓰레드가 작업할 로직을 구현하는 메소드다. 병렬 기능은 없다
start() 가 병렬처리를 해주는 메소드다. run() 을 실행하면서 위와 같은 결과를 나오게 해준다
public static void main(String[] args) {
....
runnableSample.run();
threadSample.run();
System.out.println("==============");
System.out.println("==============");
System.out.println("==============");
runnableSample.start();
threadSample.start();
}
쓰레드 사용에 유의할 점이 존재한다
실행이 종료된 쓰레드는 다시 start() 를 할 수 없다. IllegalThreadStateException 발생한다
인스턴스를 재생성 후 실행해야한다.
쓰레드의 상태
쓰레드에는 다음과 같은 상태를 가진다
- NEW : 쓰레드 객체가 생성되고 start() 호출이 안된 상태
- RUNNABLE : 실행 대기중이며 언제든지 실행 상태로 변할 수 있다
- WAITING : 일시정지 상태로 다른 쓰레드의 중지를 대기하는 상태
- TIMED_WAITING : 일정시간 동안 일시정지 대기하는 상태
- BLOCKED : 공유중이 객체를 다른 쓰레드가 사용중일 때 락이 걸려서 풀리기 기다리는 상태
- TERMINATED : 실행을 마친 상태
다음은 쓰레드의 상태를 제어하는 메소드 내역이며, 취소선은 쓰레드의 안정성을 해친다고 비권장하는 메소드다
interrupt() | 일시 정지 상태의 쓰레드에서 InterruptedException 예외를 발새시켜, 실행 대기 상태나 종료 상태로 만든다 |
notify() notifyAll() |
동기화 블록 내에서 wait() 에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 만든다 |
suspend() 에 의해 일시 정지 상태인 쓰레드를 실행 대기 상태로 만든다. 비권장으로 notify(), notifyAll() 을 대신 사용한다 |
|
sleep(long millis) sleep(long millis, int nanos) |
주어진 시간 동안 쓰레드를 일시 정지 상태로 만든다. 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다. |
join() join(long millis) join(long millis, int nanos) |
join() 을 호출한 쓰레드는 일시 정지 상태가 된다. 실행 대기 상태로 가려면, join() 메소드를 가지는 쓰레드가 종료되거나, 매개값으로 주어진 시간이 지나야한다. |
wait() wait(long millis) wait(long millis, int nanos) |
동기화 블록 내에서 쓰레드를 일시 정지 상태로 만든다. 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다. 시간이 주어지지 않으면 notify() notifyAll() 에 의해 실행 대기 상태로 갈 수 있다 |
쓰레드를 일시 정지 상태로 만든다. resume() 으로 다시 실행 대기 상태가 된다 비권장으로 wait() 을 대신 사용한다 |
|
yield() | 실행 중에 우선순위가 동일한 다른 쓰레드에게 실행을 양보하고 실행 대기 상태가 된다 |
쓰레드를 즉시 종료한다 |
쓰레드 우선순위
쓰레드는 우선순위(priority)를 갖고 있다. 우선순위가 높은 쓰레드가 실행 상태를 더 많이 가진다
우선순위는 개발자가 코드로 제어가 가능하다. 우선순위 범위는 1~10 을 가지며 1 이 가장 낮고 10 이 가장 높다
우선순위가 없으면 디폴트로 5 의 값을 가지고 시작한다
쓰레드는 우선순위 상수를 제공하기도 한다
우선순위 제어는 간단하다
setPriority() 를 사용하면 된다
public static void main(String[] args) {
// 쓰레드 우선 순위 테스트
for (int i = 1; i <= 30; i++) {
Thread thread = new PriorityTest("thread " + i);
if (i != 30) {
thread.setPriority(Thread.MIN_PRIORITY);
} else {
thread.setPriority(Thread.MAX_PRIORITY);
}
thread.start();
}
}
// 쓰레드 우선 순위 테스트용
class PriorityTest extends Thread {
public PriorityTest(String name) {
setName(name);
}
@Override
public void run() {
for (int i = 0; i < 2000000000; i++) {
}
System.out.println("Thread getName() = " + getName());
}
}
Main 쓰레드
모든 Java 애플리케이션은 main 쓰레드로 시작한다
public static void main(String[] args) 가 바로 main 쓰레드다
main 쓰레드만으로 사용하면 싱글 쓰레드 애플리케이션이라 하고
다른 쓰레드를 생성해서 사용하면 멀티 쓰레드 애플리케이션이라 한다
public static void main(String[] args) {
// 메인 쓰레드
System.out.println(Thread.currentThread().getName());
}
동기화
싱글 쓰레드 애플리케이션에서는 한개의 쓰레드로 작업하니 객체 공유 문제가 없지만
멀티 쓰레드 애플리케이션에서는 여러개의 쓰레드가 하나의 객체를 공유해서 작업하는 상황이 생긴다
다음은 멀티 쓰레드 상태에서 하나의 객체를 공유했을 때 예제이다
public class ThreadStudyMain {
public static void main(String[] args) {
// 동기화, 객체를 공유한다면?
Calc calc = new Calc();
Order1 order1 = new Order1();
order1.setCalc(calc);
order1.start();
Order2 order2 = new Order2();
order2.setCalc(calc);
order2.start();
}
}
class Calc {
private int count;
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + " : " + this.count);
}
}
class Order1 extends Thread {
private Calc calc;
public void setCalc(Calc calc) {
this.setName("Order 1");
this.calc = calc;
}
@Override
public void run() {
calc.setCount(5000);
}
}
class Order2 extends Thread {
private Calc calc;
public void setCalc(Calc calc) {
this.setName("Order 2");
this.calc = calc;
}
@Override
public void run() {
calc.setCount(10000);
}
}
order1 은 count 필드값 5000 으로 바꾸고 2초간 일시정지가 되었다
곧바로 order2 는 count 필드값 10000 으로 변경한다. 일시정지가 끝난 order1 실행이 되면?
order1 은 5000 이 아니라 10000 을 출력하게 된다.
이를 방지하기 위해 임계영역(critical section) 을 지정하게 된다. 자바에서는 임계영역을 지정하기 위해 동기화(synchronized) 메소드와 동기화 블록을 제공한다.
동기화 메소드는 메소드에 synchronized 키워드를 붙이면 끝난다
class Calc {
....
public synchronized void setCount(int count) {
this.count = count;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + " : " + this.count);
}
}
동기화 블록은 특정 영역만 동기화 시키는 것으로 다음과 같이 사용한다
public void setCount(int count) {
synchronized (this) { // 동기화 블록 synchronized(객체) 공유객체는 자기자신을 넣는다
this.count = count;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + " : " + this.count);
}
}
데드락
교착상태라고 한다. 둘 이상의 쓰레드가 공유 객체 때문에 계속 기다리는 상태를 말한다
다음은 공유 객체를 서로 접근하려는 예제이다.
public class ThreadStudyMain {
public static final Object LOCK_1 = new Object();
public static final Object LOCK_2 = new Object();
public static void main(String[] args) {
LockThread1 lockThread1 = new LockThread1();
LockThread2 lockThread2 = new LockThread2();
lockThread1.start();
lockThread2.start();
}
static class LockThread1 extends Thread {
@Override
public void run() {
synchronized (LOCK_1) { // 동기화 블록 synchronized(객체) 공유객체는 자기자신을 넣는다
System.out.println("lockThread1 : LOCK_1 잡음 ");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
System.out.println("lockThread1 : LOCK_2 기다림 ");
synchronized (LOCK_2) {
System.out.println("lockThread1 : LOCK_1 & LOCK_2 ");
}
}
}
}
static class LockThread2 extends Thread {
@Override
public void run() {
synchronized (LOCK_2) { // 동기화 블록 synchronized(객체) 공유객체는 자기자신을 넣는다
System.out.println("lockThread2 : LOCK_2 잡음 ");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
System.out.println("lockThread2 : LOCK_1 기다림 ");
synchronized (LOCK_1) {
System.out.println("lockThread2 : LOCK_2 & LOCK_1 ");
}
}
}
}
}
과연 결과는 어떻게 나올까?
서로 끝나길 무한정 기다리는 데드락이 발생한다.
'개발 & 방법론 > Java' 카테고리의 다른 글
12주차 : 애노테이션 (0) | 2021.02.25 |
---|---|
11주차 과제 : enum (0) | 2021.02.18 |
9주차 : 예외처리 (0) | 2021.02.14 |
자바 8주차 : 인터페이스 (0) | 2021.02.13 |
7주차 : 패키지 (0) | 2021.01.01 |