우리도 음악을 들으며 공부를 하거나 커피를 마시며 수다를 떨듯, 대부분 프로그램은 매서드가 하나씩 실행되지 않는다.
이렇게 두가지를 동시에 하는 것을 멀티 태스킹이라고 한다.
자바가 멀티 태스킹을 하는 형태는 멀티 프로세싱과 멀티 스레드가 있다.
멀티 프로세싱은 서로 다른 프로세스가 같은 시간 단위 내에 동시에 실행되는 것을 의미한다. 이 프로세스는 대체로 CPU당 하나씩, 병렬로 실행하는 것이 좋다(현재 컴퓨터에는 CPU가 쿼드코어(4개) 이상부터 옥타코어(8개) 등 달려있다). 그러나 멀티 프로세싱의 경우 프로세스들 끼리 변수를 공유하지 않아서 프로세스들 사이의 통신을 해야한다. 이 때, 직렬화와 유사하게 프로세스의 필드(데이터 타입, 매서드 등등)를 알려주는데, 이를 마샬링이라고 한다. 직렬화는 매서드까지 전달하진 않지만, 마샬링은 매서드까지 전달한다. 또, 이렇게 마샬링을 통해 전달된다 하더라도 프로세스 사이의 문맥 교환으로 인해 과도한 작업량과 시간 소모가 크다.
(문맥 교환 : 사용중인 CPU에서 다른 CPU로 이동할 때 현재 프로세스 상태를 보관하고, 대기하면서 새로운 프로세스 작업을 진행하는 것을 의미함)
그렇기 때문에 멀티 스레드가 더 많이 사용되는데, 프로세스가 단위인 멀티 프로세스와 다르게 멀티 스레드는 '실행'이 단위 시간 내에 동시에 실행된다.
멀티 스레드나 멀티 프로세스나 둘 다 인간의 눈에는 동시에 처리하는 것 처럼 보이지만 실제로 컴퓨터는 동시에 실행하지 않고, 다른 행동을 멈추어두고 우선적으로 처리하는 것이다(타임 슬라이싱).
그렇기 때문에 멀티 스레드가 더 많이 사용되는데, 프로세스가 단위인 멀티 프로세스와 다르게 멀티 스레드는 '실행'이 단위 시간 내에 동시에 실행된다.
멀티 스레드나 멀티 프로세스나 둘 다 인간의 눈에는 동시에 처리하는 것 처럼 보이지만 실제로 컴퓨터는 동시에 실행하지 않고, 다른 행동을 멈추어두고 우선적으로 처리하는 것이다(타임 슬라이싱). 이 동작은 주로 CPU가 한다. 프로세싱과 다르게 스레드는 같은 CPU에서 공간을 관리하기 때문에 오버헤드가 줄어드는 장점이 있다. 이런 스레드는 하드디스크 까지 가져가서 데이터를 처리할 경우 연산이 느려질 위험이 있기 때문에 램에 다운로드하며, 하드 디스크에서 가져오는 것을 가능하면 일어나지 않는 쪽으로 한다.
멀티 스레딩의 간단한 예시는 다음과 같다.
package ch13;
class Sub implements Runnable{
@Override
public void run() {
for (int i = 1; i < 11; i++) {
System.out.println("서브 스레드 : "+i);
try {
Thread.sleep(500);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public class ThreadEx01 {
//메인 스레드
public static void main(String[] args) {
Thread t1=new Thread(new Sub());
t1.start();
for (int i = 1; i < 6; i++) {
System.out.println("메인 쓰레드 : "+i);
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
멀티 스레드는 OS(운영체제)가 들고 있는 기능이며, 자바가 이를 가져와서 이용하는 것이다. 이를 시스템콜(system call)이라고 한다. 이 시스템 콜을 이용하기 위해서는 os와 JAVA 사이에 약속이 있어야 한다.
먼저, 메인 매서드 내부의 스레드가 메인 스레드이다. 메인 스레드 안의 sleep 매서드가 있다. 이 매서드는 millesecond를 매개변수로 받는데, 즉, 1000이 1초이다. 따라서, 이 스레드는 1분에 한번씩 sysout 출력을 하게 된다. 이 스레드가 잠들면, JVM이 다른 스레드를 실행시킨다. 이 때, 잠을 자는 도중 발생할 오류에 대비해 try-catch 구문을 사용하자.
JVM이 잠을 자는 사이에, 서브 스레드가 실행된다. 이 때 서브 스레드가 어떤 스레드인지 알려주기 위해(타게팅), 스레드 매서드 안에 서브 스레드가 있는 클래스를 매개변수로 넣는다.
서브 스레드를 불러오는 매서드가 있는데, 그 매서드가 start() 매서드이다. 서브 스레드를 정의하는 것은 인터페이스(os, JAVA) 간의 약속에 의해 결정되는데, 스레드는 스택을 가져야만 하는데, 그 스택을 만들기 위해 반드시 클래스를 지정하고, 이를 타겟팅 해야한다. 그렇기 때문에 클래스 이름은 아무렇게나 지어도 되지만, 반드시 Runnable 인터페이스로 지정하며, 매서드의 이름은 run으로 설정한다. 이것은 약속이니까 반드시 지키도록 하자.
서브 스레드를 지정하는 방식에 implement도 있지만, extend로 지정하는 방식도 있는데, 보통의 툴에서 implement를 사용하면 반드시 run을 사용하라는 구문 오류를 제시하지만, extend를 이용할 경우 구문오류를 제시해주지 않는 툴이 있기 때문에 implement를 사용하는 것을 추천한다.
그러나 이 멀티 스레드 방식도 한계가 있다. 다음의 코드를 보자.
다음 코드는 10000원에서 3초를 소모해 20000원을 다운 받아, 총 30000원을 만드는 코드의 예시이다.
package ch13;
class DownloadThread implements Runnable{
int data = 10000;
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
data=data+20000;
System.out.println("금액 다운로드 종료");
}
}
public class ThreadEx02 {
public static void main(String[] args) {
System.out.println("프로그램 시작");
System.out.println("---");
DownloadThread dt=new DownloadThread();
Thread t1=new Thread(dt);
t1.start();
System.out.println("금액은 : "+dt.data);
}
}
이 코드의 결과는 다음과 같다.
보다시피 출력의 결과가 20000원이 되지 않았다. 왜일까?
이 코드의 경우 정상적이라면 서브 스레드 호출 -> 메인 스레드 진행 -> 서브 스레드에서 3초간 멈춤 -> 메인 스레드에서 결과 호출의 형태를 취해야 하는데, 메인 스레드가 너무 빠르게 진행되서 서브 스레드에서 3초 후의 결과를 만들기 전에 메인 스레드에서 먼저 결과를 도출해버린 탓이다.
이 때는 다음처럼 해결한다.
package ch13;
class DownloadThread implements Runnable{
int data = 10000;
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
data=data+20000;
System.out.println("금액 다운로드 종료");
}
}
public class ThreadEx02 {
public static void main(String[] args) {
System.out.println("프로그램 시작");
System.out.println("---");
DownloadThread dt=new DownloadThread();
dt.run();
System.out.println("금액은 : "+dt.data);
}
}
다음처럼 메인 스레드에게 서브 스레드의 시작까지 통제하는 방법이 있다. 이렇게 되면 결과는,
정상적으로 출력이 된다.
전자처럼 두 스레드가 따로따로 진행되는 방식을 비동기 프로그램이라고 하고, 후자처럼 두 스레드가 하나의 스레드처럼 진행되는 방식을 동기 프로그램이라고 한다.
(이 때, 데이터의 동기화는 데이터의 일치여부를 묻지만, 프로그램에서의 동기화는 알고리즘의 순서를 묻는다.)
다음과 같은 예시를 보자.
라면과 고기를 함께 먹고싶어 함께 준비한다.
순서는 다음과 같을 것이다.
1. 냄비준비
2. 냄비에 물 넣기
3. 물 끓이기
4. 라면과 스프 넣기
5. 라면 끓이기
6. 라면 완성
7. 프라이팬 준비
8. 고기를 올리기
9. 익으면 고기 뒤집기
이렇게 순서대로 진행하게 되면 물이 끓는 시간과, 라면을 끓이는 시간을 낭비해야하는 문제가 생긴다. 이것이 동기 프로그램의 문제점이다.
만약에 그래서, 물을 끓이는 것 부터 동생에게 맡기고(서브 스레드) 나는 고기를 구우러 간다고 가정하자. 그러면 음식은 빠르게 만들어질 것이다. 그러나, 내가 고기를 다 굽고 난 시점에 라면이 완성되었는지, 혹은 동생은 내가 고기를 다 구웠는지 알 수 없어진다. 이것이 비동기 프로그램의 문제이다. 서브 스레드와 메인 스레드가 서로 완성되는 시점이 반드시 일치하지 않는다는 점이다. 이 결과를 보장하는 기술이 promise 기술이며, 자바에서는 이 데이터를 확인하는 일련의 작업을 콜백이라고 한다. 이렇게 콜백을 하여 비동기 프로그램을 처리하는 것을 비동기 처리라고 한다.
다음이 자바에서 callback 를 구현하는 예시이다.
package ch13;
interface Callback{
void printMoney(int money);
}
class MoneyChange{
int money=10000;
public void accept(Callback callback) {
//은행에 인출 요청을 해서 20000원을 받을거임(2초 걸림)
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
money=money+20000;
callback.printMoney(money);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
public class ThreadEx03 {
public static void main(String[] args) {
MoneyChange mc=new MoneyChange();
mc.accept(new Callback() {
@Override
public void printMoney(int money) {
System.out.println("통장의 잔액은 "+money);
}
});
for (int i = 1; i < 6; i++) {
System.out.println("메인 스레드 : "+i);
try {
Thread.sleep(1000);
} catch (Exception e) {
}
}
}
}
메인에서 MoneyChange 클래스를 호출을 통해 불러낸다. 그렇기 때문에 accept 함수 내의 오버라이딩 전까지는 메인 스레드에서 진행된다(runnable과 run을 익명 클래스로 묶었음).
메인 스레드에서 콜백 타입의 accept 클래스를 통해 2초 후에 printMoney를 통해 money 값을 받는다. printMoney 매서드를 가리키기 위해 인터페이스 내에 선언한다. 이는 콜백을 통해 2초 후에 메인 메서드가 서브 메서드에게서 불러오게 된다. 물론 서브 메서드에서 System.out.println("통장의 잔액은 "+money);를 적어도 값은 같지만 이렇게 될 경우 메인 매서드에서 서브 매서드의 값을 가져오지 못하게 된다. 이렇게 되면 메인 매서드와 서브 매서드 사이의 결과값이 보장되지 않게 된다.
메인 매서드는 이후에도 1초에 한번씩 출력하는 매서드를 가지고 있는데, 매서드를 통해 2초에 함께 출력되게 된다.
'JAVA' 카테고리의 다른 글
19. GUI (0) | 2020.04.20 |
---|---|
18. 소켓 통신 (0) | 2020.04.14 |
16. 익명 클래스 (0) | 2020.04.13 |
JAVA 실습 10. 공공데이터를 이용하여 코로나 공공마스크 지원 약국 주소찾기. (0) | 2020.04.10 |
JAVA 실습 9. 공공데이터와 Json을 이용한 항공데이터 조회하기 (0) | 2020.04.07 |