Thread
Thread개념을 보려면 일단 프로그램이 동작되는 개념을 이해해야 한다.
우리가 사용하는 프로그램이 설치되는 곳(위치하는 곳)은 HDD이다.
HDD에 있는 프로그램의 실행파일을 실행하면 CPU가 실행에 필요한 데이터를 메모리에 적재한다.
이렇게 메모리에 올라간 프로그램을 프로세스(Process)라고 한다. 쉽게 말해서 실행 중인 프로그램을 프로세스라고 할 수 있는 것이다. 이 때 프로세스는 하나의 프로그램인데 이 프로그램 내부에 존재하는 작업 단위를 Thread라고 한다.
하나의 프로세스는 하나 이상의 Thread로 이루어지며 간단하게 생각해서 우리가 실습에 활용하는 main메서드 하나가 하나의 Thread로 동작된다. 즉 우리가 만든 예제프로그램이 실행되면 프로세스 내부에 적어도 하나의 Thread는 존재하는 것이다.
더 깊이 들어가면 하나의 Thread가 동작될 때 운영체제가 가지는 여러 handle을 활용하는데 이 부분은 운영체제의 API를 이용하는 프로그래밍(보통 C언어에서 winapi사용)으로 공부하기 바란다.
일단 간단히 정리해서 하나의 프로그램이 동작되면 메모리에 프로세스가 존재하며 이 프로세스를 이루는 작업 단위를 Thread라고 한다는 것을 이해하도록 한다.
이 Thread를 이해하기 위해서 컴퓨터의 동작을 들여다보자. 하나의 프로세스는 한 개 이상의 Thread로 동작된다고 하였는데 이것은 CPU가 처리하는 작업 단위가 Thread라는 것을 의미하며 Single-threading방식은 하나의 thread를 모두 처리한 후 다음 thread를 처리하는 방식이며 Multi-threading방식은 여러 thread를 번갈아 가며 처리하는 방식을 말한다.
하나의 CPU는 한번에 하나의 기계어 명령만 수행하는데 우리가 현재 사용하는 컴퓨터로 여러 프로세스를 동시에 동작시킬 수 있는 것(게임, 음악재생, 파일다운로드 받기 등을 동시에 하는 것)은 CPU가 thread를 돌아가며 처리하기 때문.
여러 프로세스가 동시에 처리되는 것은 Multi-tasking(Multi-processiong)이라고도 부른다.
이는 multi-threading가 기반이어야 하며 이보다 범위가 큰 개념이라고 할 수 있다.
그러면 Thread가 활용되는 예로 간단한 게임을 생각해보자.
제한 시간 내에 어떤 미션을 수행하는 프로그램이 있다.
이 프로그램은 미션을 수행하는 동작과 제한시간을 검사하는 동작이 필요할 것이다.
그럼 하나의 프로그램에서 시간도 체크하면서 미션도 동작해야 하는데 하나의 main메서드로 표현하기는 어렵다. 이 때 제한시간을 체크할 수 있는 thread를 동작시키고 동시에 미션을 진행하는 thread를 동작시켜 실행되어야 한다.
위와 같은 경우에 thread를 사용할 수 있는데 자바에서는 Thread를 구현하기 위해 Thread클래스와 Runnable인터페이스를 제공한다.
위 문서를 통해 Thread클래스와 Runnable인터페이스의 멤버들을 한번 읽어보고 이를 통해 자바에서 스레드를 생성하고 제어하는 방법을 알아보도록 하자.
1~100까지 출력하는 기능(main)과 A~Z까지 출력하는 기능을 멀티 thread로 동작시키는 예제
첫 번째 방법 - Thread클래스를 상속하여 Thread를 구현하는 방법
예제코드
package exam;
class Thread_A extends Thread {
@Override
public void run() { // 새로운 thread에서 시작 지점
for (char i = 'A'; i <= 'Z'; i++) {
System.out.print(i);
}
}
}
public class Exam {
public static void main(String[] args) {
Thread_A thA = new Thread_A();
thA.start();
for (int i = 1; i <= 100; i++) {
System.out.printf("%3d ", i);
if (i % 10 == 0)
System.out.println();
}
}
}
실행결과
위 예제에서 thread는 두 개이다.
main 스레드와 거기에서 생성된 tha스레드
이 두 스레드를 CPU가 번갈아 가며 처리하므로 위와 같이 두 출력이 번갈아 출력된다.
따라서 실행 시마다 출력결과는 다르다.
구현한 개념은 Thread클래스에 public void run(){} 메서드가 있는데
이는 Thread객체의 start()메서드 호출 시 run()메서드를 새로운 스레드로 생성하고 동작시킨다.
두 번째 방법 - Runnable인터페이스를 상속하여 Thread를 구현하는 방법
예제코드
package exam;
class Thread_A implements Runnable {
@Override
public void run() { // 새로운 thread에서 시작 지점
for (char i = 'A'; i <= 'Z'; i++) {
System.out.print(i);
}
}
}
public class Exam {
public static void main(String[] args) {
Thread_A thA = new Thread_A();
Thread th = new Thread(thA);
th.start();
for (int i = 1; i <= 100; i++) {
System.out.printf("%3d ", i);
if (i % 10 == 0)
System.out.println();
}
}
}
실행결과
첫 번째 방법과 결과는 같다.
구현한 개념
Runnable인터페이스에는 단 하나의 메서드가 존재한다. (abstract public void run();)
이것을 구현한 클래스가 Thread_A클래스이고 이 클래스는 Thread객체 생성 시 전달이 가능하다.
Thread_A로 생성된 thA객체를 전달받아 생성된 Thread객체(th)에서 start()메서드 호출 시 새로운 스레드에서 동작된다.
보통 두 번째 방법을 사용하는 것을 권장한다. (이에 대한 자세한 내용은 여기서 언급하기 어려우므로 객체지향 디자인 패턴을 공부하도록 한다.)
위와 같은 개념으로 main thread에서 하위 thread를 구현하여 동시에 여러 작업을 병렬적으로 동작시킬 수 있다.
다음은 스레드의 사용 개념으로 알아둘 스레드 제어에 관한 내용을 확인한다.
Thread Control(스레드 제어)
자바에서 스레드는 개발자 마음대로 완벽하게 제어할 수는 없다. (CPU스케줄링은 운영체제를 통해 제어된다.)
다만 어느 정도 원하는 방향을 정해줄 수는 있다. 직접 제어 할 수 있는 작업은 각 스레드들의 우선순위 설정을 통해 실행 비중을 조절하거나 종속 스레드 여부 등을 설정하는 작업, 그리고 동기화 처리 정도이다.
이를 하나씩 확인하도록 한다.
Priority(우선순위)
Thread에서 우선순위는 먼저 하고 나중에 하는 개념이 아니라 실행 시 빈도(비중)를 지정하는 것이다.
Thread클래스에 다음과 같은 상수 필드가 존재하고
이 값을 setPriority(int newPriority)메서드에 전달하여 변경이 가능하다.(default값은 NORM_PRIORITY)
필드 이름을 클릭해서 지정된 상수 값을 확인할 수 있다.(1~10까지 범위)
예제를 통해 확인해 본다.
예제코드
package exam;
class Thread_A implements Runnable {
@Override
public void run() { // 새로운 thread에서 시작 지점
while (true) {
System.out.println("+++++++++++++++++++++++");
}
}
}
class Thread_B implements Runnable {
@Override
public void run() { // 새로운 thread에서 시작 지점
while (true) {
System.out.println("-----------------------");
}
}
}
public class Exam {
public static void main(String[] args) {
Thread thA = new Thread(new Thread_A());
Thread thB = new Thread(new Thread_B());
System.out.println("thA의 Priority : " + thA.getPriority());
System.out.println("thB의 Priority : " + thB.getPriority());
thA.start();
thB.start();
for (int i = 0; i < 1000000000; i++);
System.exit(0);
}
}
실행결과는 항상 다르겠지만 대체로 +와 -가 비슷하게 출력될 것이다.
두 스레드의 우선순위를 1과 10으로 변경하여 실행해본다.
우선순위에 따른 실행 비중이 달라지는 것을 확인한다.
package exam;
class Thread_A implements Runnable {
@Override
public void run() { // 새로운 thread에서 시작 지점
while (true) {
System.out.println("+++++++++++++++++++++++");
}
}
}
class Thread_B implements Runnable {
@Override
public void run() { // 새로운 thread에서 시작 지점
while (true) {
System.out.println("-----------------------");
}
}
}
public class Exam {
public static void main(String[] args) {
Thread thA = new Thread(new Thread_A());
Thread thB = new Thread(new Thread_B());
thA.setPriority(Thread.MAX_PRIORITY); // 10
thB.setPriority(Thread.MIN_PRIORITY); // 1
System.out.println("thA의 Priority : " + thA.getPriority());
System.out.println("thB의 Priority : " + thB.getPriority());
thA.start();
thB.start();
for (int i = 0; i < 1000000000; i++);
System.exit(0);
}
}
thA의 우선순위가 최대이므로 여러 번 실행 해보면 +가 더 자주 많이 출력되는 것을 볼 수 있다.
반대로 우선순위를 바꿔서 실행해 보면 -가 더 많이 출력될 것이다.
이를 통해 스레드들의 우선순위(비중)을 조절할 수 있는 개념을 이해하면 된다.
이 우선순위 값은 다음과 같은 상황에 따라 조절할 수 있다.
10 : 위기관리 작업(실시간 매우 빠른 처리)
7~9 : 상호작용, 이벤트처리
4~6 : IO관련 작업
2~3 : 백그라운드 작업
1 : 다른 작업이 없을 때 실행(CPU가 가장 여유 있을 때)
스레드의 종료
위 우선순위 예제에서 main메서드의 System.exit(0)를 주석처리하고 실행해보자.
main스레드가 종료되었음에도 불구하고 하위 thread들이 계속 실행되는 것을 볼 수 있다.
먼저 스레드를 종료하는 방법에 대해 알아보자.
스레드를 종료하는 방법은 flag를 사용하여 flag상태에 따라 stop()메서드를 호출하는 것인데 권장하지 않는다.
대신 interrupt()메서드를 사용하는 것이 좀더 나은 방법이다.
interrupt()메서드는 호출되면 현재 수행 중인 명령을 바로 중지시킨다.
만약 interrupt()메서드 호출 시 Object클래스의 wait메서드나 Thread클래스의 join, sleep메서드가 호출된 경우
InterrupedException을 발생시킨다. 따라서 interrupt()메서드로 스레드를 종료하도록 하는 경우 예외처리가 필요하다.
아래 예제를 보면 스레드의 interrupt여부를 이용하여 상태를 지정하고 있다.
해당 thread에 interrupt()메서드가 호출되면 isInterrupt()의 결과가 false가 되어 while문이 종료되고 run메서드가 종료된다. (thread종료)
사용예제
package exam;
class Thread_A implements Runnable {
@Override
public void run() { // 새로운 thread에서 시작 지점
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("+++++++++++++++++++++++");
}
} catch (Exception e) {
System.out.println("Thread_A InterruptedException : " + e.getMessage());
} finally {
System.out.println("Thread_A 종료");
}
}
}
class Thread_B implements Runnable {
@Override
public void run() { // 새로운 thread에서 시작 지점
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("-----------------------");
}
} catch (Exception e) {
System.out.println("Thread_B InterruptedException : " + e.getMessage());
} finally {
System.out.println("Thread_B 종료");
}
}
}
public class Exam {
public static void main(String[] args) {
Thread thA = new Thread(new Thread_A());
Thread thB = new Thread(new Thread_B());
thB.setPriority(Thread.MAX_PRIORITY); // 10
thA.setPriority(Thread.MIN_PRIORITY); // 1
System.out.println("thA의 Priority : " + thA.getPriority());
System.out.println("thB의 Priority : " + thB.getPriority());
thA.start();
thB.start();
for (int i = 0; i < 1000000000; i++)
;
thA.interrupt();
thB.interrupt();
System.out.println("main 종료");
// System.exit(0);
}
}
실행결과
만약 interrupt()를 호출한 스레드의 상태가 wait, sleep, join등의 상태라면 InterruptedException이 발생되어 어플리케이션이 종료되기 때문에 예외처리를 통해 정상적으로 종료되도록 구현할 필요가 있다.(위 예제의 try~catch~finally부분)
Daemon Thread(데몬 스레드)와 join()
전제: 자바에서는 모든 스레드가 종료되어야 어플리케이션(JVM)이 종료된다.
어플리케이션 동작에서 main메서드는 메인 스레드이고 그 외에 생성되는 스레드는 모두 main의 동작을 보조하기 위해 생성되는 것이 일반적이다. 하지만 main스레드가 종료되어도 위에서 interrupt()를 사용한 것과 같이 직접 종료를 하지 않으면 각 스레드가 모두 실행을 완료할 때까지 어플리케이션이 종료되지 않는다. 따라서 일반적으로는 main thread가 종료되면 함께 종료되도록 해야 한다.
그런데 JVM에서 가비지컬렉터와 같이 분리된 스레드로 별도의 작업을 진행하는 스레드가 필요할 수 있다.
그리고 그러한 스레드를 관리하기 위해 직접 종료(stop, interrupt등)를 하지 않더라도 main이 종료되면 함께 종료되는 스레드를 지정할 수 있는데 이를 Daemon Thread(데몬 스레드)라 한다.
앞의 예제에서 thA와 thB를 데몬 스레드로 지정해 본다.
실습예제
package exam;
class Thread_A implements Runnable {
@Override
public void run() { // 새로운 thread에서 시작 지점
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("+++++++++++++++++++++++");
}
} catch (Exception e) {
System.out.println("Thread_A InterruptedException : " + e.getMessage());
} finally {
System.out.println("Thread_A 종료");
}
}
}
class Thread_B implements Runnable {
@Override
public void run() { // 새로운 thread에서 시작 지점
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("-----------------------");
}
} catch (Exception e) {
System.out.println("Thread_B InterruptedException : " + e.getMessage());
} finally {
System.out.println("Thread_B 종료");
}
}
}
public class Exam {
public static void main(String[] args) {
Thread thA = new Thread(new Thread_A());
Thread thB = new Thread(new Thread_B());
thB.setPriority(Thread.MAX_PRIORITY); // 10
thA.setPriority(Thread.MIN_PRIORITY); // 1
thB.setDaemon(true);
thA.setDaemon(true);
System.out.println("thA의 Priority : " + thA.getPriority());
System.out.println("thB의 Priority : " + thB.getPriority());
System.out.println("thA의 Daemon여부 : " + thA.isDaemon());
System.out.println("thB의 Daemon여부 : " + thB.isDaemon());
thA.start();
thB.start();
for (int i = 0; i < 1000000000; i++);
System.out.println("main 종료");
}
// System.exit(0);
}
실행결과
...
...
main스레드가 종료되면 하위 스레드들이 모두 종료되지 않고 각자 처리를 모두 한 후에 종료하는 것을 확인할 수 있다.
위와 같이 별도의 스레드가 부모 스레드의 종료여부와 관계 없이 동작되는데
만약 main스레드가 종료될 때까지만 필요한 스레드라면
Daemon으로 지정하여 자동으로 종료되도록 제어할 수 있다.
여기에 만약 특정 Thread가 종료 되기 전까지 다른 스레드가 기다려야 될 필요가 있는 기능이라면?
join()메서드를 활용할 수 있다.
다음과 같이 예제를 작성한다.
package exam;
class Thread_A implements Runnable {
@Override
public void run() { // 새로운 thread에서 시작 지점
System.out.println("───────────────────────");
System.out.println("Thread A 시작");
for (int i = 0; i < 5; i++) {
System.out.println("+++++++++++++++++++++++");
}
System.out.println("Thread A 종료");
System.out.println("───────────────────────");
}
}
class Thread_B implements Runnable {
@Override
public void run() { // 새로운 thread에서 시작 지점
System.out.println("───────────────────────");
System.out.println("Thread B 시작");
for (int i = 0; i < 5; i++) {
System.out.println("-----------------------");
}
System.out.println("Thread B 종료");
System.out.println("───────────────────────");
}
}
public class Exam {
public static void main(String[] args) {
System.out.println("───────────────────────");
System.out.println("main 시작");
for (int i = 0; i < 3; i++) {
System.out.println("|||||||||||||||||||||||");
}
Thread thA = new Thread(new Thread_A());
Thread thB = new Thread(new Thread_B());
try {
thA.start();
System.out.println("main: thA종료 대기");
thA.join();
thB.start();
System.out.println("main: thB종료 대기");
thB.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 3; i++) {
System.out.println("|||||||||||||||||||||||");
}
System.out.println("main 종료");
System.out.println("───────────────────────");
}
}
실행결과
join의 동작을 위 예제를 통해 이해할 수 있다.
즉 하위 스레드가 끝날 때까지 상위 스레드가 기다린 후에 동작한다.(interruptedException 주의)
스레드 동기화(synchronized)
멀티스레드 사용 시 하나의 객체에 여러 스레드가 접근하는 경우에 발생되는 문제를 동기화 문제라 한다.
다음 예제 코드를 보자.
package exam;
class Account {
private int money = 1000;
public int draw(int val) {
if (money >= val) {
money -= val;
return val;
} else {
return 0;
}
}
public int getBalance() {
return money;
}
}
class CashDispenser implements Runnable {
private Account account = null;
private int drawMoney;
public CashDispenser(Account account, int drawMoney) {
this.account = account;
this.drawMoney = drawMoney;
}
@Override
public void run() {
int val = account.draw(drawMoney);
System.out.println(Thread.currentThread().getName() + ((val == 0) ? "잔액 부족!" : "인출금액 : " + val + "원"));
System.out.println(Thread.currentThread().getName() + "잔액 : " + account.getBalance() + "원");
}
}
public class Exam {
public static void main(String[] args) {
Account account = new Account();
CashDispenser atm = new CashDispenser(account, 600);
CashDispenser internetBanking = new CashDispenser(account, 600);
Thread thAtm = new Thread(atm);
thAtm.setName("ATM");
Thread thIb = new Thread(internetBanking);
thIb.setName("InternetBanking");
thAtm.start();
thIb.start();
}
}
실행결과(정상적인 결과)
반복해서 실행해보면 동기화 문제발생 결과 확인 가능
위와 같이 하나의 객체를 여러 스레드가 사용하는 경우 위와 같이 문제가 발생할 수 있다.
CPU가 thAtm스레드를 동작시키다가 인출이 가능한지 조건만 확인하는 명령을 처리하고
다음 명령을 실행하기 전에 thIb 스레드를 처리하도록 제어가 넘어간 경우
thIb에서 인출이 가능한지 조건을 확인했을 때 역시 가능하다는 동작이 되어 버리면
두 스레드에서 모두 인출하는 코드를 동작시켜 버리기 때문이다
이는 자바의 실행 메모리에 대한 내용을 이해하면 좋겠지만 여기서 다루면 내용이 많아질 것 같으니 다음에 자바 메모리에 대한 내용을 따로 정리하도록 하자.
일단 위 코드는 두 스레드가 하나의 객체에 접근하기 때문에 발생되는 문제인 것이다.
이를 해결하기 위해서 동시에 접근되는 객체의 내부에서 특정 부분을 하나의 스레드가 실행하는 중일 때 다른 스레드가 해당 코드를 실행하려 한다면 대기하도록 하는 작업을 동기화 라고 한다.
두 가지 방법으로 적용이 가능한데
첫 번째는 다음과 같이 동기화 메서드를 지정하는 것이다.
두 스레드가 동시에 접근하면 if문이 두 스레드 모두 참인 경우가 생길 수 있으니 draw()메서드 자체를 동기화 하는 것이다. 위와 같이 설정하면 A스레드가 draw메서드를 실행 중 이라면 B스레드는 대기하고 있다가 A스레드가 draw메서드를 끝내면 B스레드가 draw메서드를 동작한다. 따라서 값이 동기화된다.
두 번째 방법은 동기화 블록을 지정하는 방법이다.
동기화 시킬 특정 영역을 지정하는 방법으로 1번보다 권장하는 방법이다.
내부적으로 동작은 거의 같은 개념으로 동작되나 동기화 되는 범위가 다르다는 것이 차이점이다.
이것보다 더 효율적인 방법으로는 java.util.concurrent.locks 패키지의 클래스들을 활용하는 방법이 있다.
스레드를 관리하는 것은 운영체제와도 관계가 있으며 별도의 챕터로 다뤄야 될만큼 어려운 부분이다.
따라서 여기서는 스레드의 개념 정도만 알아두고 이 후 필요한 시점에 집중해서 공부하는 것을 추천한다.
'교육자료 > Java' 카테고리의 다른 글
Java IO 바이트스트림(byte기반) (0) | 2017.06.30 |
---|---|
Java IO - File class (0) | 2017.06.30 |
Java Exception(예외처리) (0) | 2017.06.28 |
Java Collection part2 (0) | 2017.06.28 |
Java Collection part1 (0) | 2017.06.27 |