스레드_멀티 스레드
1. 프로세스와 스레드
프로세스는 운영체제에서 실행 중인 하나의 애플리케이션을 말한다.
사용자가 애플리케이션을 실행하면 운영체제로부터 실행에 필요한 메모리를 할당받아 애플리케이션의 코드를 실행하는데 이것을 프로세스라고 한다.
그리고 스레드는 프로세스 내부에서의 코드의 실행 흐름을 말한다.
스레드의 사전적 의미는 한가닥 실이다. 즉 프로그래밍에서 스레드는 한 가지 작업을 위해 순차적으로 실행할 코드를 한가닥 실처럼 길게 늘어 놓았다고하여 유래된 명칭이다.
프로세스와 스레드는 멀티태스킹이 가능하다. 멀티 태스킹은 두 가지 이상의 작업을 동시에 처리하는 것을 말한다.
프로세스의 경우 실행되는 프로세스 마다 필요한 메모리를 할당해주고 병렬로 실행시킨다.
이것이 멀티 프로세스이다.
스레드의 경우에도 멀티태스킹이 가능하다.
하나의 프로세스에서 두 가지 작업이 동시에 처리될 수 있도록 할 수 있다.
이것이 멀티 스레드이다.
멀티 프로세스의 경우, 각 프로세스는 운영체제로부터 할당 받은 자신의 메모리를 통해 실행된다.
따라서 하나의 프로세스에서 오류가 발생해도 다른 프로세스의 실행에는 영향을 미치지 않는다.
그러나 멀티 스레드의 경우, 하나의 프로세스 내부에서 실행되기 때문에 하나의 스레드가 예외를 발생시키면 프로세스가 종료될 수 있어 다른 스레드에도 영향을 미친다.
따라서 멀티 스레드를 사용할 경우, 예외 처리에 매우 주의를 기울여야한다.
2. 메인 스레드
자바의 모든 애플리케이션은 메인 스레드가 main메소드를 실행하면서 시작된다.
메인 스레드는 main메소드의 코드를 위에서 아래로 순차적으로 실행한다.
그리고 main메소드의 코드를 전부 실행하거나, return문을 만나면 실행이 종료된다.
또한 메인 스레드는 필요에 따라 작업 스레드를 만들어 여러 개의 스레드를 병렬로 처리할 수 있다.
즉 멀티 스레드를 생성하여 멀티태스킹을 수행할 수 있다.
여기서 한가지 주의할 점이 있다.
싱글 스레드, 즉 메인 스레드만 존재하는 애플리케이션은 메인 스레드가 종료되면 프로세스가 종료된다.
그러나 멀티 스레드의 경우, 메인 스레드가 종료되더라도 아직 실행 중인 작업 스레드가 존재한다면 프로세스는 종료되지 않고 모든 스레드가 처리될 때까지 실행된다.
3. 작업 스레드 생성과 실행
멀티 스레드로 실행하는 애플리케이션을 개발하기 위해서는 우선 애플리케이션에서 몇 개의 작업을 병렬로 실행할지 결정해야한다.
그리고 그 작업의 개수 만큼 작업 스레드를 생성해야한다.
어떤 자바 애플리케이션이건 무조건 메인 스레드가 존재한다.
따라서 메인 작업을 제외하고, 추가적인 병렬 작업의 수 만큼 작업 스레드를 생성해야한다.
자바에서는 작업 스레드가 객체로서 생성되기 때문에 객체 생성을 위한 클래스가 필요하다.
작업 스레드 객체는 java.lang.Thread클래스를 통해 직접 생성할 수 있다.
또한 Thread클래스를 상속하는 하위 클래스를 선언하여 작업 스레드 객체를 생성할 수 있다.
1) Thread클래스로부터 직접 생성
java.lang.Thread클래스로부터 작업 스레드 객체를 직접 생성하기 위해서는 아래와 같이 Runnable 타입을 매개변수로 하는 생성자를 호출해야한다.
- Thread thread = new Thread(Runnable target);
Runnable은 인터페이스이기 때문에 구현 클래스를 선언하여 구현 객체를 대입해야한다.
Runnable에는 run메소드가 정의되어있는데, 구현 클래스에서 run메소드를 재정의할 때 작업 스레드가 실행할 코드를 작성해줘야한다.
- Class Task implements Runnable { public void run() { 작업 스레드 실행 코드 } }
- Runnable task = new Task();
- Thread thread = new Thread(task);
또한 코드 절약을 위해 Runnable 익명 객체를 사용하는 방법이 있다. 이 방법이 더 자주 사용된다.
- Thread thread = new Thread( new Runnable() { public void run() { 작업 스레드 실행 코드 } } );
그리고 작업 스레드는 Thread클래스의 run메소드 호출을 통해 사용된다.
- thread.start();
아래의 예제 코드는 작업 스레드 사용의 예를 보여준다.
Runnable인터페이스의 구현 클래스인 BeepTask클래스가 존재한다.
해당 클래스에는 run메소드가 재정의 되어있으며, 0.5초 간격으로 비프음을 5번 발생시키는 작업 스레드 실행 코드가 작성되어있다.
BeepPrintExample2클래스에는 메인 스레드가 존재하며, 해당 클래스의 목적은 0.5초 간격으로 5번 비프음과 "띵"이라는 출력을 동시에 발생시키는 것이다.
메인 스레드 내부를 살펴보자.
Runnable 타입의 beepTask변수가 선언되었으며, 해당 변수는 BeepTask객체를 참조한다.
그리고 Thread 타입의 thread변수가 선언되어있으며, 해당 변수는 작업 스레드 객체를 참조한다. 작업 스레드 생성자의 매개값으로는 beepTask변수가 사용되었다.
그리고 thread.start();를 통해 작업 스레드가 실행되는데, 작업 스레드 생성자의 매개값으로 사용된 beepTask가 참조하는 BeepTask객체의 run()메소드가 실행되는 것이다.
그리고 작업 스레드의 실행과 동시에 메인 스레드의 코드도 계속하여 실행된다.
즉 작업 스레드에서는 비프음 출력, 메인 스레드에서는 문자열 출력을 담당하고 그것이 병렬로 진행되는 것이다.
BeepPrintExample3클래스는 Runnable인터페이스의 구현 클래스를 별도로 선언하지 않고, Runnable 익명 구현 객체를 통해 작업 스레드를 생성하고 실행하는 예를 보여준다.
package sec01.exam02;
import java.awt.Toolkit;
public class BeepTask implements Runnable {
public void run() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i = 0; i < 5; i++) {
toolkit.beep();
try { Thread.sleep(500); } catch(Exception e) {}
}
}
}
package sec01.exam02;
public class BeepPrintExample2 {
public static void main(String[] args) {
Runnable beepTask = new BeepTask();
Thread thread = new Thread(beepTask);
thread.start();
for(int i = 0; i < 5; i++) {
System.out.println("띵");
try { Thread.sleep(500); } catch(Exception e) {}
}
}
}
package sec01.exam02;
import java.awt.Toolkit;
public class BeepPrintExample3 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i = 0; i < 5; i++) {
toolkit.beep();
try { Thread.sleep(500);} catch(Exception e) {}
}
}
});
thread.start();
for(int i = 0; i < 5; i++) {
System.out.println("띵");
try { Thread.sleep(500); } catch(Exception e) {}
}
}
}
2) Thread 하위 클래스로부터 생성
Runnable인터페이스를 사용하지 않고, Thread클래스의 하위 클래스를 사용하여 작업 스레드를 생성할 수도 있다.
Thread클래스에도 run메소드가 정의되어 있으며, Thread클래스를 상속하는 하위 클래스(작업 스레드 클래스) 선언하고 run메소드를 재정의하여 작업 스레드가 실행할 내용을 작성해 줄 수 있다.
그 후 Thread 타입의 변수를 선언하고, 해당 변수가 작업 스레드 객체를 참조하도록 해주면 된다.
- public class WorkerThread extends Thread { public void run() { 작업 스레드 실행 코드 } }
- Thread thread = new WorkerThread();
이 경우 또한 코드 절약을 위해 작업 스레드 클래스를 선언하지 않고, Thread클래스의 익명 객체를 통해 작업 스레드를 사용할 수 있다.
- Thread thread = new Thread() { public void run() { 작업 스레드 실행 코드 } };
아래의 예제 코드는 Thread클래스의 하위 클래스를 통해 작업 스레드를 사용하는 예를 보여준다.
BeepThread클래스가 존재한다.
해당 클래스는 Thread클래스의 하위 클래스, 즉 작업 스레드 클래스이다.
BeepThread클래스에는 run메소드가 재정의되어 있으며, 해당 메소드에는 비프음을 발생시키기 위한 작업 스레드 코드가 작성되어있다.
BeepPrintExample4클래스는 메인 스레드를 갖고 있는 클래스이다.
메인 스레드 내부를 살펴보자.
메인 스레드 내부에는 Thread 타입의 thread 변수가 선언되어 있고, 해당 변수는 작업 스레드 객체를 참조하고 있다.
thread.start();를 통해 작업 스레드가 실행된다.
그리고 문자열을 출력하는 메인 스레드가 작업 스레드와 동시에, 즉 병렬 처리된다.
BeepPrintExample5클래스는 작업 스레드 클래스 선언 없이 익명 자식 객체를 통해 작업 스레드를 생성하는 예를 보여준다.
package sec01.exam04;
import java.awt.Toolkit;
public class BeepThread extends Thread {
@Override
public void run() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i = 0; i < 5; i++) {
toolkit.beep();
try { Thread.sleep(500); } catch(Exception e) {}
}
}
}
package sec01.exam04;
public class BeepPrintExample4 {
public static void main(String[] args) {
Thread thread = new BeepThread();
thread.start();
for(int i = 0; i < 5; i++) {
System.out.println("띵");
try { Thread.sleep(500); } catch(Exception e) {}
}
}
}
package sec01.exam05;
import java.awt.Toolkit;
public class BeepPrintExample5 {
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i = 0; i < 5; i++) {
toolkit.beep();
try { Thread.sleep(500); } catch(Exception e) {}
}
}
};
thread.start();
for(int i = 0; i < 5; i++) {
System.out.println("띵");
try { thread.sleep(500); } catch(Exception e) {}
}
}
}
3) 스레드의 이름
각 스레드는 자신의 이름을 갖고 있다.
스레드의 이름이 코드 작성에 있어서 매우 중요한 부분은 아니지만, 디버깅을 할 때 어떤 스레드가 어떤 작업을 실행하고 있는지 확인하기 위해 사용된다.
메인 스레드는 main이라는 이름 갖고 있다.
작업 스레드는 자동적으로 Thread-n이라는 이름으로 설정된다. n은 스레드의 번호이다.
만약 스레드의 이름을 변경하고 싶다면 setName메소드를 사용하면 된다.
그리고 스레드의 이름을 알고 싶다면 getName메소드를 사용하면 된다.
- thread.setName("스레드 이름");
- thread.getName();
setName, getName메소드는 Thread클래스의 인스턴스 메소드이기 때문에 스레드 객체를 참조하고 있어야한다.
만약 스레드 객체를 참조하고 있지 않다면, Thread클래스의 정적 메소드인 currentThread메소드를 통해 현재 스레드의 참조를 얻을 수 있다.
- Thread thread = Thread.currentThread();
아래의 예제 코드는 스레드 이름의 설정과 호출의 예를 보여준다.
ThreadA, ThreadB클래스는 Thread클래스의 하위 클래스, 즉 작업 스레드 클래스이다.
ThreadA클래스의 생성자를 보면 setName메소드를 통해 ThreadA객체, 즉 작업 스레드의 이름을 ThreadA로 설정해준다.
그리고 해당 작업 스레드 클래스에는 getName메소드를 통해 스레드 이름을 가져오는 작업 스레드 실행 코드가 작성되어있다.
ThreadB클래스는 작업 스레드의 이름을 설정해주는 코드가 작성되어 있지는 않다.
그리고 ThreadA클래스와 마찬가지로 getName메소드를 통해 스레드 이름을 가져오는 코드가 작성되어있다.
메인 스레드를 가진 ThreadNameExample클래스 내부를 살펴보자.
우선 메인 스레드에 대해 getName메소드를 사용하기 위해 currentThread메소드를 통해 메인 스레드 객체를 얻고 있다.
그리고 각각 ThreadA객체, ThreadB객체를 생성하고 실행을 한다.
실행 내용은 스레드 이름을 가져오는 것인데 ThreadA의 경우 생성자를 통해 스레드 이름을 ThreadA로 바꿔줬기 때문에 ThreadA가 출력된다.
그러나 ThreadB의 경우 스레드 이름을 별도로 설정해주지 않았기 때문에 Thread-1이 출력된다.
package sec01.exam06;
public class ThreadA extends Thread {
public ThreadA() {
this.setName("ThreadA");
}
public void run() {
for(int i = 0; i < 2; i++) {
System.out.println(getName() + "가 출력한 내용");
}
}
}
package sec01.exam06;
public class ThreadB extends Thread {
public void run() {
for(int i = 0; i < 2; i++) {
System.out.println(getName() + "가 출력한 내용");
}
}
}
package sec01.exam06;
public class ThreadNameExample {
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
System.out.println("프로그램 시작 스레드 이름: " + mainThread.getName());
ThreadA threadA = new ThreadA();
System.out.println("작업 스레드 이름: " + threadA.getName());
threadA.start();
ThreadB threadB = new ThreadB();
System.out.println("작업 스레드 이름: " + threadB.getName());
threadB.start();
}
}
4. 동기화 메소드
메인 스레드만 사용하는 싱글 스레드 프로세스의 경우, 어떠한 객체를 사용하는 스레드는 하나일 수 밖에 없다.
그러나 작업 스레드도 사용하는 멀티 스레드 프로세스의 경우, 어떠한 객체를 여러 스레드가 공유해서 사용할 수 있다.
이때 문제가 발생할 수 있는데 어떠한 객체에 대한 스레드의 작업이 동시에 진행될 경우, 각 스레드가 의도한 대로 작업이 진행되지 않을 수 있다.
즉 개발자가 의도하지 않은 결과가 산출될 수 있다.
이러한 문제를 해결하기 위해 자바에서는 동기화 메소드라는 기능을 제공한다.
1) 동기화 메소드(synchronized method)
위에서 말한 문제를 해결하기 위한 하나의 방법은 어떠한 스레드가 작업 중인 객체에 대해서 다른 스레드가 접근할 수 없도록 제한하는 것이다.
이처럼 멀티 스레드 프로세스에서 단 하나의 스레드만 접근할 수 있는 코드 영역을 임계 영역(critical section)이라고 한다.
그리고 이러한 임계 영역을 지정하기 위해 사용되는 것이 동기화 메소드(synchronized method)이다.
특정 스레드가 사용할 객체 내부의 동기화 메소드를 실행하면 즉시 객체에 잠금을 걸어 다른 스레드가 동기화 메소드를 실행하지 못하도록 한다.
동기화 메소드는 아래와 같이 메소드를 선언할 때 synchronized 키워드를 사용하면 된다.
- public synchronized void method() { 임계 영역; }
만약 동기화 메소드가 여러 개 존재하는 경우, 어떤 스레드가 특정 동기화 메소드를 실행할 때에는 다른 스레드가 해당 동기화 메소드는 물론 다른 동기화 메소드도 실행할 수 없다.
그러나 일반 메소드는 실행 가능하다.
아래의 예제 코드는 동기화 메소드 사용의 예를 보여준다.
우선 Calculator클래스를 살펴보자.
Calculator클래스는 일반 메소드인 getMemory메소드와 동기화 메소드인 setMemory메소드가 선언되어 있다.
그리고 User1클래스와 User2클래스는 작업 스레드 클래스이다.
그리고 두 클래스에는 Calculator객체의 동기화 메소드인 setMemory를 호출하는 작업 실행 코드를 갖고 있다.
이제 메인 스레드를 갖고 있는 MainThreadExample클래스를 살펴보자.
우선 공유 객체 Calculator를 참조하는 calculator변수가 선언된다.
그리고 User1작업 스레드가 생성되고, setCalculator메소드를 통해 calculator변수를 User1스레드는 자신의 calculator필드에 저장한다.
즉 calculator필드가 공유 객체 Calculator를 참조하게 되는 것이다.
그 후 calculator필드가 참조하고 있는 공유 객체의 setMemory메소드를 호출하여 공유 객체의 memory변수 값에 100을 대입한다.
setMemory메소드는 memory변수에 값을 대입한 후 2초 후에 memory변수 값을 출력한다.
User2작업 스레드도 생성된 후, User1작업 스레드와 동일한 작업을 수행하며 setMemory메소드를 동해 공유 객체의 memory변수 값에 50을 대입한다.
코드 작성자의 의도는 User1, User2스레드가 각각 100, 50의 memory변수 값을 출력하는 것이다.
이것을 위해 setMemory메소드가 동기화 메소드로 지정된 것이다.
만약 동기화 메소드로 지정되지 않았다면, User1스레드에서 메모리 설정 후 출력하기 까지의 2초 동안 User2스레드에서 setMemory메소드가 실행되어 공유 객체의 memory변수 값은 50으로 변경될 것이다.
결국 User1, User2스레드 모두 50을 출력할 것이다.
하지만 setMemory메소드를 동기화 메소드로 지정해주므로써, User1스레드가 setMemory메소드 실행을 종료할 때까지 User2스레드가 setMemory에 접근하지 못하도록 막을 수 있다.
그 결과 User1스레드는 100을 출력하고 User2스레드는 50을 출력할 수 있게 된다.
package sec01.exam07;
public class Calculator {
private int memory;
public int getMemory() {
return memory;
}
public synchronized void setMemory(int memory) {
this.memory = memory;
try {
Thread.sleep(2000);
} catch(InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + ": " + this.memory);
}
}
package sec01.exam07;
public class User1 extends Thread {
private Calculator calculator;
public void setCalculator(Calculator calculator) {
this.setName("User1");
this.calculator = calculator;
}
public void run() {
calculator.setMemory(100);
}
}
package sec01.exam07;
public class User2 extends Thread {
private Calculator calculator;
public void setCalculator(Calculator calculator) {
this.setName("User2");
this.calculator = calculator;
}
public void run() {
calculator.setMemory(50);
}
}
package sec01.exam07;
public class MainThreadExample {
public static void main(String[] args) {
Calculator calculator = new Calculator();
User1 user1 = new User1(); //User1 스레드 생성
user1.setCalculator(calculator); //공유객체 설정
user1.start(); //User1 스레드 시작
User2 user2 = new User2(); //User2 스레드 생성
user2.setCalculator(calculator); //공유객체 설정
user2.start(); //User2 스레드 시작
}
}
출처: 혼자 공부하는 자바(신용권)