
멀티태스킹(multi-tasking)은 하나의 CPU가 여러가지 작업을 번갈아 처리하는 방식으로 동시에 처리하는 것처럼 보이지만, 실은 CPU가 빠르게 작업을 전환하면서 동시에 처리하는 듯한 효과를 낸다.
멀티프로세스(multi-process)는 여러 CPU 또는 CPU 코어를 이용해서 여러 프로세스를 동시에 실행하는 것을 말한다. 프로세스간 메모리를 공유하지 않아 안정성이 높다는 특징이 있다.
예로는 웹 브라우저가 탭마다 별도의 프로세스를 띄우는 구조를 생각해볼 수 있다.
멀티스레드는 하나의 프로그램(프로세스) 내에서 여러 작업들(작은 프로세스)을 동시에 수행하는 것을 말한다. 스레드가 단위가 되어 동작하며, 프로세스 내에서 메모리를 공유한다는 특징을 가진다.
멀티스레드는 프로세스보다 생성/소멸 비용이 낮고, 각 스레드가 메모리를 공유하며 독립적으로 동작하기에 빠른 응답에 최적화되어있다. 다만, 메모리를 공유하기 때문에, 여러 스레드에서 하나의 메모리에 동시에 접근하면 동기화 관련 문제가 발생할 수 있다.
스레드(thread)가 이번주차의 주된 내용이다:)
thread): 프로세스 내에서 실행되는 가장 작은 작업 단위자바는 기본적으로 멀티스레드르 지원하고 있다고한다. 최소 하나의 스레드, 즉 메인 스레드를 가지고 시작하며 필요에 따라 추가적인 스레드를 생성해 동시에 여러 작업을 처리할 수 있다.
모든 자바 프로그램은 메인 스레드가 main() 메소드를 실행하며 시작된다. main() 메소드의 첫 코드부터 아래로 순차적으로 실행되며, 마지막 코드를 실행, return문을 만나거나 stop 신호가 온 뒤 처리코드가 있을 때 실행이 종료된다.
메인 스레드에서 작업 스레드들을 만들어 병렬로 코드를 실행할 수 있다.
즉, 멀티스레드를 생성해 멀티 태스킹을 수행할 수 있다.
싱글스레드의 경우에는 메인 스레드가 종료되면, 프로세스 역시 종료되지만, 멀티스레드의 경우 하나의 스레드라도 실행 중이라면 프로세스가 종료되지 않는다.
스레드를 생성하는 방법은 하나가 아니라 다양하다.
방법1: Thread 클래스 상속받기
class Player extends Thread {
public void run() {
//Thread 실행 (start()) 시, 실행되는 메소드
}
}
public class Main {
public static void main(String[] args) {
Thread t1 = new Player();
t1.start();
}
}
위와 같이 Thread 클래스를 상속받은 클래스(여기서는 Player)를 만든 뒤에, 해당 클래스 내에 public void run()의 메소드를 오버라이딩해주어 스레드를 생성 및 사용할 수 있다.
start() 메소드
Player라는 클래스가 Thread를 상속받았기 때문에, 메인 메소드에서 해당 클래스를 생성해 변수에 저장해준다. 변수명을 사용해 t1.start() 메소드를 실행하면 스레드가 실행된다.
스레드를 상속한 클래스에서 오버라이딩한 메소드는 run()이지만, 스레드 객체를 생성해 start() 메소드를 실행해주면, 해당 스레드가 실행되며 run() 메소드가 실행된다.
방법2:
Runnable인터페이스 구현하기
class MyRunnable implements Runnable {
public void run() {
...
}
}
public class Main {
public static void main(String[] args) {
Thread t2 = new Thread(new MyRunnable());
t2.start();
}
}
위와 같이 Runnable 인터페이스를 구현하는 클래스를 만들어 스레드를 생성하는 방법 역시 존재한다. Thread를 상속한 클래스와 마찬가지로 run()이라는 메소드를 오버라이딩해야한다.
new Thread(new Runnable());
앞서 스레드를 상속받은 클래스는 바로 해당 클래스를 생성하여 직접 사용한 것과 다르게, Runnable 인터페이스를 구현하는 클래스는 Thread 객체를 생성하는 매개변수로 전달하는 방식을 사용하고 있다. Runnable 객체를 Thread의 매개변수로 넘겨주는 것이다.
이렇게 Runnable 인터페이스를 사용해 구현하면, 해당 클래스가 다른 상위클래스를 상속받을 수 있기 때문에 Thread 클래스를 상속해서 구현하는 것보다 유용할 것 같다..
Runnable 인터페이스는 스레드가 수행할 실제작업을 코드로 작성하고 정의한다고한다. Thread는 반면 스레드를 생성/관리하는데 필요한 모든 기능을 제공한다. Thread에는 앞서 사용한 start()와 같은 메소드가 정의되어 있어 스레드를 시작하고 스케줄링할 수 있다.
따라서 Runnable 인터페이스를 구현하면 스레드가 할 작업이 정의된 것이지, 스레드로 동작할 기능이 구현되어 있지 않다는 것 같다. 따라서 Runnable 객체를 Thread 객체에 주입하여, 스레드가 해당 작업을 실행하도록 하는 것인..듯하다.
방법3: 익명 클래스 사용하기
public static void main(String[] args) {
Thread t3 = new Thread(new Runnable() {
public void run() {
...
}
});
t3.start();
}
따로 클래스를 생성하지 않고 메인 메소드 내에서 바로 메소드 작성해 사용 가능한..듯 한다. 클래스명이 따로 없는 익명의 클래스.
방법4: 람다식 사용하기
... Thread t = new Thread(() -> { System.out.println("스레드 실행"); }); t.start();
익명 클래스와 비슷하게 바로 메소드를 구현하여서 사용하는 느낌..인 듯한데, ->와 같이 람다식의 문법을 사용해 스레스 생성 및 사용이 가능한 듯 하..다!
class MyRunnable implements Runnable {
String myName;
public MyRunnable(String name) {
myName = name;
}
public void run() {
for (int i=0; i<10; i++) {
System.out.println(myName+i);
}
}
}
public class TestThread {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable("First Thread"));
Thread t2 = new Thread(new MyRunnable("Second Thread"));
t1.start();
t2.start();
}
}
Runnable 인터페이스를 사용해서 스레드를 생성 및 실행 시키는 실습코드였다. 왠지 이상적으로는 나름 순서대로 혹은 체계대로 실행될 것이라고 생각하게되지만,
실제 실행결과를 보면, 전혀 예상하지 못한 순서대로 실행문이 찍혀있는 것을 확인할 수 있다. 실행순서는 참고로 실행을 할 때마다 변동된다.
스레드의 실행은 OS에서 관리하기 때문에, 코드를 어떻게 작성하든 스레드가 코드를 작성한 의도대로 동작하리라고 보장할 수 없을 때가 많다.
(이를 그래도 조금이나마 관리할 수 있는 Thread의 메소드가 존재하긴한다. 다만, 완벽하게 스레드의 작업순서를 관리하기는 쉽지 않은 것 같았다.)
class Horse implements Runnable {
String name;
private int sleepTime;
private final static Random generator = new Random();
public Horse(String name) {
this.name = name;
sleepTime = generator.nextInt(3000);
}
public void run() {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(name+"말이 경주를 완료하였습니다.");
}
}
public class TestThread1 {
public static void main(String[] args) {
Thread t1 = new Thread(new Horse("질풍"));
Thread t2 = new Thread(new Horse("번개"));
Thread t3 = new Thread(new Horse("적토마"));
t1.start();
t2.start();
t3.start();
}
}
같은 실습에서 경주말을 달리는 코드를 작성하였다. 역시 Runnable 인터페이스를 사용해 스레드를 생성했다. 스레드마다 이름을 지정하여 구분시킬 수 있게하였다.
t1,t2,t3의 순서대로 start() 메소드를 작성해두었기에, 일반적으로 생각하면 t1 -> t2 -> t3의 순서대로 실행되고, 도착할 것 같지만 이 역시도 실행결과가 매번 바뀌고, 저 순서대로 진행되지 않는다. 호출은 코드가 작성된 순서대로 이루어질지 몰라도, 스레드의 시작은 순전히 OS(운영체제)에 달려있다.
| 메소드 | 설명 |
|---|---|
Thread() | 기본 생성자로, 새로운 스레드 객체를 생성해줌 |
Thread(String name) | 이름을 가진 스레드 객체를 생성할 수 있음 |
Thread(Runnable target) | Runnable 인터페이스를 구현한 객체를 인수로 받아, 해당 객체의 run() 메소드를 실행하는 스레드를 생성 |
start() | 스레드를 시작. 해당 메소드는 run() 메소드를 호출해 스레드에서 실행할 코드를 실행함 |
run() | 스레드에서 실행될 코드를 작성하는 메소드 |
sleep() | 현재 실행 중인 스레드를 지정된 시간(밀리초)동안 일시정지 |
interrupt() | 현재 스레드에 신호를 보내 중단하도록 알림을 보냄. 다만, 멈추고 싶은 스레드에 신호를 보내도 무시하고 멈추지 않을 수 있어, 처리하는 코드를 작성해야함 |
join() | 다른 스레드가 종료될 때까지 현재의 스레드를 대기시킴. 시간을 지정할 수도 있음 |
위의 코드는 메인코드이고, Player1 클래스는 스레드를 상속받은 클래스, Player2는 Runnable 인터페이스를 구현하는 클래스이다.
p1-p3, th1-th3의 스레드를 실행하는 코드를 순서대로 호출하고 있지만, 실행결과는 위와 같이 랜덤하게 일어나고 있다. 앞의 예와 동일하게 실행할 때마다, 실행되는 스레드의 순서가 매번 바뀐다.
도달 지점(goal)에 도착할 때까지 차가 달리는데, 경기 중 특정 확률로 고장이 발생할 수 있어 고장 신호가 오면 해당 자동차는 경기를 중단하게되는 시나리오의 실습이다.
interrupt() 신호를 사용하여 고장 신호를 보내는 것을 구현한다.
public class Car extends Thread {
String name;
int speed;
volatile boolean stop = false;
public Car (String name, int speed) {
this.name = name;
this.speed = speed;
System.out.println(name+"생성");
}
public void run() {
for (int i=0; i<=자동차경주.goal; i++) {
System.out.println(name+":"+i+"km...");
//5% 확률로 interrupt 신호 보내기
if ((int)(Math.random()*1000)%100<5){
System.out.println(name+" 고장고장고장!");
this.interrupt();
}
if(Thread.interrupted()) {
System.out.println(name+":"+i+"km지점에서 중단 인터럽트 감지 -> 쓰레드 종료!");
return; //안전하게 종료
}
//외부에서 보낸 interrupt 신호도 감지
try {
Thread.sleep(300);
} catch (InterruptedException e) {
System.out.println(name+": sleep 도중 인터럽트 발생: "+e.getMessage());
return;
}
}
System.out.println(name+"도착!!");
}
}
일단 자동차(Car) 클래스의 필드를 살펴보면, 차의 이름을 저장할 문자열 name, 속도를 저장한 정수 speed가 있다.
이외에도 차가 움직이는 중인지, 멈췄는지 상태를 기록하는 stop이라는 필드가 존재한다. 해당 필드에는 다른 필드와는 다르게 volatile이라는 키워드가 붙어있다. 찾아보닌 volatile 키워드는 변수의 가시성을 보장하는 역할을 한다고 한다.
스레드 실행 시, 실제 실행되는 메소드인 run()에는 goal에 도착하기 전까지 반복문이 실행되며 차가 달리게된다. 일정 확률로 인터럽트 신호가 보내질 수 있으며, 인터럽트 신호가 들어오면 해당 신호를 확인하는 조건문을 통해 안전하게 종료된다.
public class 자동차경주 {
static int goal = 30;
public static void main(String[] args) {
int goal = 100;
Thread car1 = new Car("붕붕카",1000);
Thread car2 = new Car("스포츠카",3000);
Thread car3 = new Car("세발자전거",100);
System.out.println("==============자동차 경주==============");
car1.start();
car2.start();
car3.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
car2.interrupt();
}
}
위가 실습4를 위한 메인 메소드의 코드이다. 세 개의 스레드를 만들어 각각을 하나의 차로 두게된다. start() 메소드는 car1 -> car2 -> car3의 순서로 작성되고 호출되겠지만, 어떤 스레드가 어떤 순서로 실행될지는 OS에 달렸다.
Thread.sleep(3000)이라는 코드를 통해 일정 시간(여기서는 3000ms) 메인 메소드가 일시중지 되었다가 마지막 코드인 interrupt()가 실행된다.
자동차(Car) 클래스에 적혀있던, 외부의 인터럽트를 감지하는 코드가 위의 코드에서 인터럽트 신호를 보내기 때문에 적혀있었던 것이다. (처음에는 랜덤하게 인터럽트 신호를 주는데 왜 저런 코드가 있는지 바로 깨닫지 못했다...)
실행결과는 위의 부분이 잘리긴했지만, 아래와 같다.
소감
뭔가 처음 예상한 스레드는 나름 순서도 지정할 수 있고, 원하는 순서대로 동작할 것이라는 느낌이었는데 전혀 아니라 의외였다. Thread 클래스를 상속하는 경우와 Runnable 인터페이스를 구현하는 등 다양한 방식으로 스레드를 생성 및 실행시킬 수 있었다.. 아, 그리고 스레드 관련 메소드가 많다..ㅎㅎ 전부 이해하려면 많이 써봐야할 것 같은..? 느낌이었다.