Goal
- 프로세스와 스레드
- 스레드의 구현과 실행
- start()와 run()
- 싱글 스레드와 멀티스레드
- 스레드의 우선순위
- 스레드 그룹(thread group)
- 데몬 스레드(deamon thread)
- 스레드의 실행 제어
- 스레드의 동기화
프로세스와 스레드
프로세스(process)
- 간단히 설명하면 실행 중인 프로그램(program)이다.
- 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 된다.
- 프로세스는 프로그램을 수행하는데 필요한 자원(데이터, 메모리 등) 그리고 스레드로 구성되어 있으며 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 바로 스레드이다.
- 모든 프로세스 에는 최소한 하나 이상의 스레드가 존재하며, 둘 이상의 스레드를 가진 프로세스를 멀티스레드 프로세스(multi_threaded process)라고 한다.
스레드(thread)란
- 스레드(thread)란 프로세스(process) 내에서 실제로 작업을 수행하는 주체를 의미합니다.
- 모든 프로세스에는 한 개 이상의 스레드가 존재하며 작업을 수행합니다.
- 스레드를 프로세스라는 작업공간(공장)에서 작업을 처리하는 일꾼(worker)으로 생각하면 이해하기 쉬울 것이다.
- 싱글 스레드 프로세스 = 자원 + thread
- 멀티스레드 프로세스 = 자원 + thread + thread
- 하나의 프로세스가 가질 수 있는 스레드의 개수는 제한되어 있지 않으나 메로리 한 게(호출 스택의 크기)에 따라 생성할 수 있는 스레드의 수가 결정된다.
- 멀티태스킹과 마찬가지고 멀티 스레딩은 하나의 프로세스 내에서 여러 스레드가 동시에 작업을 수행하는 것이 가능하다. (실제로는 한 개의 CPU가 한 번에 단 한 가지 작업만 수행할 수 있기 때문에 아주 짧은 시간 동안 여러 작업을 번갈아 가며 수행함으로써 동시에 여러 작업이 수행되는 것처럼 보이게 하는 것이다.)
- 프로세스의 성능이 스레드의 개수에 비례하지 않으며, 하나의 스레드를 가진 프로세스 보다 두 개의 스레드를 가진 프로세스가 오히려 더 낮은 성능을 보일 수도 있다.
스레드의 구현과 실행
- 스레드를 구현하는 방법은 두 가지가 있습니다.
- Runnable 인터페이스를 구현하는 방법
- Thread 클래스를 상속받는 방법
- 이 두 가지 방법 중 어느 쪽을 사용해도 별 차이는 없지만 Thread클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문에, Runnable 인터페이스를 구현하는 방법이 일반적이다.
- Runnable 인터페이스를 구현하는 방법은 재사용성(reusability)이 높고 코드의 일관성(consistency)을 유지할 수 있다는 장점이 있기 때문에 보다 객체지향적인 방법이라 할 수 있겠다.
1.Thread 클래스를 상속
class MyThread extends Thread{
public void run() {/*작업 내용*/} // Thread클래스의 run() 을 오버라이딩
}
2.Runnable인터페이스를 구현
class MyThread implements Runnable {
public void run() {/*작업 내용*/} // Runnable 인터페이스의 추상메서드 run() 을 구현
}
- Runnable 인터페이스는 run() 메서드만 정의되어 있는 간단한 인터페이스이다. Runnable인터페이스를 구현하기 위해서 해야 할 일은 추상 메서드인 run()의 몸통을 만들어 주는 것뿐이다.
- 스레드를 구현 한다는 것은 위의 2가지 방법 중 어떤 것을 선택하건 간에, 스레드를 통해 작업하고자 하는 내용으로 run()의 몸통을 채우기만 하면 되는 것이다.
public class thread {
public static void main(String[] args) {
ThreadEx1_1 t1 = new ThreadEx1_1() ;
Runnable r = new ThreadEx1_2() ;
Thread t2 = new Thread(r);
t1.start();
t2.start();
}
public class ThreadEx1_1 extends Thread{
public void run() {
for(int i=0; i<5; i++) {
System.out.println(getName()); //조상인 Thread의 getName() 을 호출
}
}
}
public class ThreadEx1_2 implements Runnable{
public void run() {
for(int i=0; i<5; i++) {
//Thread.currentThread() - 현재 실행중인 Thread를 반환한다.
System.out.println(Thread.currentThread()); //조상인 Thread의 getName() 을 호출
}
}
}
실행 결과
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
- Runnable 인터페이스를 구현한 경우, Runnable인터페이스를 구현한 클래스의 인스턴스를 생성한 다음, 이 인스턴스를 가지고 Thread클래스의 인스턴스를 생성할 때 생성자의 매개 변수로 제공해야 한다.
- 이때 사용되는 Thread클래스의 생성자는 Thread(Runnable target)로 호출 시에 Runnable인터페이스를 구현한 클래스의 인스턴스를 넘겨줘야 한다.
- Thread클래스를 상속받으면, Thread클래스의 메서드를 직접 호출할 수 있지만, Runnable을 구현하면 Thread클래스의 static메서드인 currentThread()를 호출하여 스레드에 대한 참조를 얻어 와야만 호출이 가능하기 때문에 MythreadEx1_1에서는 간단히 getName()을 호출하면 되지만, MyThreadEx1_2에는 멤버라고는 run() 밖에 없기 때문에 Thread클래스의 getName()을 호출하려면 Thread.currentThread().getName()와 같이 해야 한다.
Thread currentThread() - 현재 실행중인 쓰레드의 참조를 반환한다.
String getName() - 쓰레드의 이름을 반환한다.
- 스레드의 이름은 다음과 같이 생성자나 메서드를 통해서 지정 또는 변경할 수 있다.
Thread(Runnable target, String name)
Thread(String Name)
void setName(String)
public static void main(String[] args) {
ThreadEx1_1 t1 = new ThreadEx1_1() ;
Runnable r = new ThreadEx1_2() ;
Thread t2 = new Thread(r,"MyThreadTask");
t1.start();
t2.start();
}
public class ThreadEx1_2 implements Runnable{
public void run() {
for(int i=0; i<5; i++) {
//Thread.currentThread() - 현재 실행중인 Thread를 반환한다.
Thread.currentThread().setName("ThreadEx1_2");
System.out.println("getName = "+Thread.currentThread().getName()); //조상인 Thread의 getName() 을 호출
}
}
}
- 다음과 같이 2가지 방법으로 이름을 변경할 수 있다.
- 여기서 스레드는 다시 재사용할 수 없다는 것을 알아야 한다 즉, 하나의 스레드에 대해 start()가 한 번만 호출될 수 있다는 뜻이다. 그래서 스레드의 작업이 한 번 더 수행되기를 원한다면 새로운 쓰래드를 다시 생성한 다음에 start()를 호출해야 한다. 하나의 스레드에 대해 start()를 두 번 이상 호출하면 실행 시에 illegalThreadStateException이 발생한다.
public static void main(String[] args) {
ThreadEx1_1 t1 = new ThreadEx1_1() ;
Runnable r = new ThreadEx1_2() ;
Thread t2 = new Thread(r,"MyThreadTask");
t1.start();
t1 = new ThreadEx1_1();
t1.start();
t2.start();
}
Start()와 run()
- 스레드를 실행시킬 때 run()이 아닌 start()를 호출한다는 것에 대해서 의문이 들어야 할 것이다.
- run()을 호출하는 것은 생성된 스레드를 실행시키는 것이 아니라 단순히 클래스에 속한 메서드 하나를 호출하는 것이다.
- 반면에 start()는 새로운 스레드가 작업을 실행하는데 필요한 호출 스택(call start)을 생성한 다음에 run()을 호출해서, 생성된 호출 스택에 run()이 첫 번째로 저장되게 한다.
- 모든 스레드는 독립적인 작업을 수행하기 위해 자신만의 호출 스택을 필요로 하기 때문에 새로운 스레드를 생성하고 실행시킬 때마다 새로운 호출 스택이 생성되고 스레드가 종료되면 작업에 사용된 호출 스택은 소멸된다.
- main 메서드에서 스레드의 start메서드를 호출한다.
- start메서드는 스레드가 작업을 수행하는 데 사용될 새로운 호출 스택을 생성한다.
- 생성된 호출 스택에 run메서드를 호출해서 스레드가 작업을 수행하도록 한다.
- 이제는 호출 스택이 2개이기 때문에 스케줄러가 정한 순서에 의해서 번갈아 가면서 실행된다.
- 위의 그림에서와 같이 스레드가 둘 이상일 때는 호출 스택의 최상위에 있는 메서드 일지라도 대기상태에 있을 수 있다.
- 스케줄러는 시작되었지만 아직 종료되지 않은 스레드들의 우선순위를 고려하여 실행 순서와 실행시간을 결정하고 각 스레드들은 작성된 스케줄에 따라 자신의 순서가 되면 지정된 시간 동안 작업을 수행한다.
(start()가 호출된 스레드는 바로 실행되는 것이 아니라는 것에 주의. 일단 대기 상태로 있다고 스케줄러가 정한 순서에 의해서 실행된다.) - 주어진 시간 동안 작업을 마치지 못한 스레드는 다시 자신의 차례가 돌아올 때까지 대기상태에 있게 되면, 작업이 마친 스레드, 즉 run()의 수행이 종료된 스레드는 호출 스택이 모두 비워지면서 이 스레드가 사용하던 호출스택은 사라진다.
- 지금까지는 main메서드가 수행을 마치면 프로그램이 종료되었으나, 위의 그림에서와 같이 main메서드가 수행을 마쳤다 하더라고 다른 스레드가 아직 작업을 마치지 않은 상태라면 프로그램이 종료되지 않는다.
(실행 중인 사용자 스레드가 하나도 없을 때 프로그램은 종료된다.)
public class MyThreadEx2_1 extends Thread{
public void run() {
System.out.println("run");
throwException();
System.out.println("run2");
}
public void throwException() {
try {
System.out.println("throwException");
throw new Exception();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws Exception{
MyThreadEx2_1 t1 = new MyThreadEx2_1();
System.out.println("t1.start전");
t1.start();
}
t1.start전
run
throwException
java.lang.Exception
at thread.MyThreadEx2_1.throwException(MyThreadEx2_1.java:14)
at thread.MyThreadEx2_1.run(MyThreadEx2_1.java:6)
run2
- 새로 생성한 스레드에서 고의로 예외를 발생시키고 printStackTrace()을 이용해서 예외가 발생한 당시의 호출스택을 출력하는 예제이다. 호출스택의 첫번째 메서드가 main메서드가 아니라 run메서드인 것을 확인하자.
(한 스레드에서 예외가 발생해서 종료되어도 다른 스레드의 실행에는 영향을 미치지 않는다.)
public static void main(String[] args) throws Exception{
MyThreadEx2_1 t1 = new MyThreadEx2_1();
System.out.println("t1.start전");
t1.run();
}
- 이 예제 역시 고의적으로 예외를 발생시켜서 호출스택의 내용을 확인할 수 있도록 했다. 이전 예제와는 달리 main메서드가 호출스택에 포함되어 있음.
싱글스레드와 멀티스레드
- 두 개의 작업을 하나의 스레드(th1)로 처리하는 경우와 두 개의 스레드(th1,th2) 로 처리 하는 경우를 가정해보자.
하나의 스레드로 두 작업을 처리하는 경우는 한 작업을 마친 후 에 다른 작업을 시작하지만, 두 개의 스레드로 작업 하는 경우에는 짧은 시간동안 2개의 스레드(th1,th2)가 번갈아 가면서 작업을 수행해서 동시에 두 작업이 처리되는 것과 같이 느끼게 한다.
- 하나의 스레드로 두개의 작업을 수행한 시간과 두개의 스레드로 두 개의 작업을 수행한 시간은 거의 같다. 오히려 두 개의 스레드로 작업한 시간이 싱글스레드로 작업한 시간보다 더 걸리게 되는데 그 이유는 스레드간의 작업전환(context switching)에 시간이 걸리기 떄문이다.
- 그래서 단순히 CPU만을 사용하는 계산작업 이라면 오히려 멀티스레드 보다 싱글스레드로 프로그래밍 하는 것이 더 효율적이다.
- 프로세스간 또는 스레드간의 전환을 스위칭이라고 한다.
- 스레드의 스위칭에 비해 프로세스의 스위칭이 더 많은 정보를 저장해야 하므로 더많은 시간이 소요된다.
public static void main(String[] args) {
// TODO Auto-generated method stub
long startTime = System.currentTimeMillis();
for(int i = 0; i<300 ; i++) {
System.out.print("-");
if(i % 100 == 0) {
System.out.println();
}
}
System.out.println("소요시간:" + (System.currentTimeMillis()-startTime));
for(int i = 0; i<300 ; i++) {
System.out.print("|");
if(i % 100 == 0) {
System.out.println();
}
}
System.out.println("소요시간:" + (System.currentTimeMillis()-startTime));
}
-
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------소요시간:1
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||소요시간:2
- -를 출력하는 작업과 | 를 출력하는 작업을 하나의 스레드가 연속적으로 처리하는 시간을 측정하는 예제이다. 컴퓨터의 성능이나 실행환경에 의해서 실행결과는 달라질 수 있다.
- 다음은 새로운 스레드를 하나 생성해서 두 개의 스레드가 작업을 하나씩 나누어서 수행한후 실행결과를 비교
public class ThreadEx5 {
static long startTime = 0;
public static void main(String[] args) {
ThreadEx5_1 th1 = new ThreadEx5_1();
th1.start();
startTime = System.currentTimeMillis();
for(int i = 0; i<300 ; i++) {
System.out.print("-");
if(i % 100 == 0) {
System.out.println();
}
}
System.out.println("소요시간1:" + (System.currentTimeMillis()-startTime));
}
}
class ThreadEx5_1 extends Thread{
static long startTime = 0;
public void run() {
for(int i = 0; i<300 ; i++) {
System.out.print("|");
if(i % 100 == 0) {
System.out.println();
}
}
System.out.println("소요시간2:" + (System.currentTimeMillis()-startTime));
}
}
-|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
----------------------------------------------------------------------------------------------------|||||||||||||||||||
|||||
----|||||||||||||||||||||||||||||||||||||||----------------------------||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|||||||||||||||||||||||||||||||--------------------------------------------------------------------
-----------------------------------------------------------------------------||----------------------|||||||||||||||||소요시간1:4
|||||||||||||||||||||||||||||||||||||||||||||||||소요시간2:1615526730952
- 이전 과는 달리 두 작업이 아주 짧은 시간동안 번갈아가면서 실행되엇으며 거의 동시에 작업이 완료 되었다.
- 위의 결과는 실행될 때마다 다른 결과를 얻을 수 있는데 그 이유는 실행 중에 예제 프로그램이 OS의 프로세스 스케줄러의 영향을 받기 때문이다. JVM의 스레드 스케줄러에 의해서 어떤 스레드가 얼마동안 실행될 것인지 결정되는 것과 같이 프로세스도 프로세스 스줄러에 의해서 실행순서와 실행시간이 결정되기 때문에 매 순간 상황에 따라 프로세스에서 할당되는 실행시간이 일정하지 않고 스레드에서 할당되는 시간 역시 일정하지 않게 된다 스레드가 이러한 불확실성을 가지고 있다는 것을 염두에 두어야 한다.
- JAVA 가 OS(플랫폼) 독립적 이라고 하지만 실제로는 OS 종속적인 부분이 몇 가지 있는데 스레드도 그 중의 하나다.
- JVM의 종류에 따라 스레드 스케줄러의 구현방법이 다를 수 있기 때문에 멀티스레드로 작성된 프로그램을 다른 종류의 OS에서도 충분히 테스트해 볼 필요가 있다.
- CPU이외의 자원을 사용하는 작업의 경우에는 싱글쓰레드 프로세스보다 멀티쓰레드 프로세스가 더 효율적이다. 예를 들면 사용자로부터 데이터를 입력받는 작업, 네트워크로 파일을 주고받는 작업, 프린터로 파일을 출력하는 작업과 같이 외부기기와의 입출력을 필요로 하는 경우
- 만일 사용자로 부터 입력받은 작업과 화면에 출력하는 작업을 하나의 스레드로 처리한다면 위의 왼쪽 그래프처럼 사용자가 입력을 마칠 때까지 아무 일도 하지 못하고 기다리기만 해야 한다.
- 그러나 두 개의 스레드로 처리한다면 사용자의 입력을 기다리는 동안 다른 스레드가 작업을 처리할 수 있기 때문에 보다 효율적인 CPU의 사용이 가능하다.
public class ThreadEx6 {
public static void main(String[] args) {
// TODO Auto-generated method stub
String input = JOptionPane.showInputDialog("아무 값이나 입력하세요");
System.out.println("입력하신 값은"+input+"입니다.");
for(int i=10; i>0; i--) {
System.out.println(i);
try {
Thread.sleep(1000);
} catch (Exception e) {
// TODO: handle exception
}
}
}
}
입력하신 값은asd입니다.
10
9
8
7
6
5
4
3
2
1
- 이 예제는 하나의 쓰레드로 사용자의 입력을 받는 작업과 화면에 숫자를 출력하는 작업을 처리하기 때문에 사용자가 입력을 마치기 전까지는 화면에 숫자가 출력되지 않다가 사용자가 입력을 마치고 나서야 화면에 숫자가 출력된다.
public static void main(String[] args) throws Exception {
ThreadEx7_1 th1 = new ThreadEx7_1();
th1.start();
String input = JOptionPane.showInputDialog("아무 값이나 입력하세요");
System.out.println("입력하신 값은" + input + "입니다.");
}
public class ThreadEx7_1 extends Thread {
public void run() {
for (int i = 10; i > 0; i--) {
System.out.println(i);
try {
sleep(1000);
} catch (Exception e) {
//
}
}
}
}
10
9
8
7
6
입력하신 값은8888입니다.
5
4
3
2
1
'자바' 카테고리의 다른 글
JAVA public, private, protected 접근자의 차이점 (0) | 2021.08.12 |
---|---|
JVM 구조 (0) | 2021.04.19 |
추상 메소드 (0) | 2021.04.08 |
클래스와 객체 (0) | 2021.03.16 |
다형성 (0) | 2021.03.16 |