[Java 기초 공부] 쓰레드 (thread) -1
프로세스(process) : 실행 중인 프로그램
프로세스 구성 : 프로그램을 수행하는데 필요한 자원(데이터와 메모리 등), 쓰레드
쓰레드 : 자원을 이영해서 실제로 작업을 수행하는 것
하나의 쓰레드 : 싱글쓰레드
둘 이상의 쓰레드 : 멀티쓰레드
프로세스 안 쓰레드의 수 = 프로세스 안 일꾼의 수로 생각하면 쉽다.
윈도우나 유닉스를 포함한 대부분의 OS는 멀티태스킹(다중 작업)을 지원하기 떄문에 여러 개의 프로세스가 동시에 수행될 수 있다.
멀티쓰레딩은 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것.
CPU의 코어가 한 번의 하나의 작업만 수행할 수 있으므로,
실제로 동시에 처리되는 작업의 갯수 = CPU 코어의 갯수
하지만, 쓰레드의 수 > CPU 코어의 수 이기 떄문에 각 코어가 아주 짧은 시간동안 여러 작업을 번갈아 가며 수행 함으로써
, 여러 작업들이 모두 동시에 수행되는 것처럼 보이게 한다.
그래서 프로세스의 성능이 단순히 쓰레드 개수에 비례 하는 것은 아니며, 하나의 쓰레드를 가진 프로세스 보다 두 개의 쓰레드를 가진 프로세스가 더 낮은 성능을 보일 수 도 있다.
쓰레드를 가벼운 프로세스, 경량 프로세스라고 부르기도 한다.
멀티쓰레딩의 장단점
- 장점
- CPU의 사용률을 향상시킨다.
- 자원을 보다 효율적으로 사용할 수 있다.
- 사용자에 대한 응답성이 향상된다.
- 작업이 분리되어 코드가 간결해진다.
- 단점
- 여러 쓰레드가 같은 프로세스 내에 자원을 공유하면서 작업을 하기 떄문에
동기화, 교착상태와 같은 문제가 있다.
- 여러 쓰레드가 같은 프로세스 내에 자원을 공유하면서 작업을 하기 떄문에
쓰레드의 구현과 실행
- Thread 클래스 상속
class MyThread extends Thread{
public void run() { /* 작업내용 */ } //클래스의 run()을 오버라이딩
}
2. Runnable 인터페이스 구현
class MyThread implements Runnable {
public void run() { /* 작업내용 */ } //인터페이스의 run()을 구현
}
별 차이는 없지만 상속은 하나의 클래스에게서만 받을 수 있기 떄문에 인터페이스를 사용하는 방법이 일반적이다.
- 또, 인터페이스로 구현하면 재사용성 증가, 코드의 일관성 유지하는 객체지향적인 방법이 된다는 장점이 존재한다.
비교 예제 (쓰레드 예제 1)
import java.lang.*;
public class ThreadEx1 {
public static void main(String[] args) {
ThreadEx1_1 t1 = new ThreadEx1_1();
Runnable r = new ThreadEx1_2();
Thread t2 = new Thread(r); //생성자 Thread(Runnable target);
//Thread t2 = new Thread(new ThreadEx1_2());
t1.start();
t2.start();
}
}
class ThreadEx1_1 extends Thread{
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(getName());
}
}
}
class ThreadEx1_2 implements Runnable{
public void run() {
for (int i = 0; i < 5; i++) {
// Thread.currentThread() - 현재 실행중인 Thread를 반환한다.
System.out.println(Thread.currentThread().getName());
}
}
}
Runnable 인터페이스로 구현한 경우 - 인터페이스를 구현한 클래스의 인스턴스를 생성한 다음, 이 인스턴스를 Theard클래스의 생성자의 매개변수로 제공해야 한다. 이렇게 함으로써 상속을 통해 run()을 오버라이딩하지 않고도 외부로부터 run을 제공받을 수 있다.
Thread클래스 상속으로 구현한 경우 - 자손 클래스에서 조상인 이 클래스의 메소드를 직접 호출할 수 있지만, Runnable을 구현하면 Theard클래스의 static메서드인 currentThread()를 호출하여 쓰레드에 대한 참조를 얻어 와야만 호출 가능하다.
static Thread currentThread() - 현재 실행중인 쓰레드의 참조를 반환한다.
String getName() 쓰레드의 이름을 반환한다.
그래서 ThreadEx1_1은 간단히 getName()을 호출하면 되지만 ThreadEx1_2의 맴버는 run()밖에 없기 떄문에 Thread.currentThread().getName() 과 같이 사용해야한다.
Thread(Runnable target, String name)
Thread(String name
void setName(String name)
//defalut : Thread-번호
쓰레드의 이름은 위와 같은 생성자나 메서드를 통해서 지정, 변경이 가능하다.
쓰레드의 실행 - start()
생성후 start()를 호출해야 실행대기 상태로 넘어가고 자신의 차례가 오면 실행상태가 된다.
실행 대기 중인 쓰레드가 없으면 곧바로 실행상태
쓰레드는 하나의 쓰레드에 start()가 한 번만 호출될 수 있다. 더 실행해야 한다면 다시 생성하고 start()를 호출 해야한다.
start()와 run()
main에서 run()을 호출하는 것은 생성된 쓰레드를 실행시키는 것이아니라 단순히 메서드를 호출하는 것일 뿐이다.
반면 start()는 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택을 생성한 다음에 run()을 호출해서, 생성된 호출스택에 run()이 첫 번째로 올라가게 한다.
모든 쓰레드는 독립적인 작업을 수행하기 이해 자신만의 호출스택을 필요로 하기 때문에 새로운 쓰레드를 생성하고 실행시킬 때마다 새로운 호출스택이 생성되고 쓰레드가 종료되면 사용된 호출스택은 소멸된다.
위와 호출스택이 2개 이상이 되면 스케줄러가 정한 순서에 의해 번갈아 가면서 작업이 실행된다.
(호출스택(LIFO)에서 가장 위에 있는게 실행중인 메서드 그러나, 위의 그림과 같이 쓰레드가 둘 이상일 때는 호출스택의 최상위라 할지라도 대기상태에 있을 수 있다.)
주어진 시간동안 작업을 마치지 못한 쓰레드는 다시 자신의 차례가 돌아올 때까지 대기상태로 있게 되며, 수행이 종료된 쓰레드는 호출스택이 모두 비워지면서 호출스택 또한 사라진다.
지금까지는 main메서드가 종료되면 프로그램도 같이 종료되었으나, 위와 그림과 같이 main메서드가 수행을 마쳤다하더라도 다른 쓰레드가 아직 작업을 마치지 못한 상태라면 프로그램은 종료되지 않는다.
class ThreadEx2 {
public static void main(String[] args){
ThreadEx2_1 t1 = new ThreadEx2_1();
t1.start
}
}
class ThreadEx2_1 extends Thread {
public void run() {
throwException();
}
public void throwException() {
try {
throw new Exception();
} catch(Exception e) {
e.printStackTrace ();
}
}
}
===========
실 행 결 과
===========
java.lang.Exception
at Thread2_1.throwException(ThreadEx2.java:15)
at Thread2_1.run(ThreadEx2.java:10)
쓰레드에서 고의로 예외를 발생시키고 예외가 발생한 당시의 호출스택을 출력하는 예제이다. 호출스택의 첫번째 메서드가 main아니다.
한 쓰레드가 예외가 발생해서 종료되어도 다른 쓰레드의 실행에는 영향을 미치지 않는다.
start()가 아닌 run()을 호출하면 호출스택이 새로 생기지않고 메인쓰레드에서 실행된다.
싱글쓰레드와 멀티쓰레드
위 그래프에서 알 수 있듯이 (a)와 (b)의 작업 수행을 한 시간은 거의 같거나 오히려 (b)가 (a)보다 더 걸리게 되는데 그 이유는 쓰레드의 작업전환에 시간이 걸리기 떄문이다.
그래서 싱글 코어에서 단순히 CPU만을 사용하는 계산작업이라면 싱글쓰레드로 프로그래밍하는 것이 더 효율적이다.
여러 쓰레드가 여러 작업을 동시에 진행하는 것 : 병행(concurrnet)
여러 쓰레드가 하나의 작업을 나눠서 진행하는 것 : 병렬(parallel)
멀티쓰레드 프로그램(ex:단순 출력 쓰레드)은 실행할때마다 다른 결과를 얻을 수 있는데 그 이유는 프로세스가 OS의 프로세스 스케줄러의 영향을 받기(쓰레드에게 할당되는 시간과 작업시간이 일정하지 않음)때문이다.
두 쓰레드가 서로 다른 자원을 사용하는 작업의 경우에는 싱글쓰레드 프로세스보다 멀티쓰레드 프로세스가 더 효율적이다. 예를 들면 사용자로부터 데이터를 입력받는 작업⑴, 네트워크로 파일을 주고받는 작업⑵, 프린터로 파일을 출력하는 작업⑶과 같이 외부기기와의 입출력을 필요로 하는 경우가 해당한다.
(1)에서 사용자에게 입력받는 시간 동안 (2),(3)이 실행 될 수 있음. 싱글 쓰레드는 같이 대기상태
쓰레드의 우선순위
쓰레드는 우선순위라는 속성을 가지고 있는데, 이 우선순위 값에따라 쓰레드가 얻는 실행시간이 달라진다. 쓰레드가 수행하는 작업의 중요도에 따라 쓰레드의 우선순위를 서로 다르게 지정하여 특정 쓰레드가 더 많은 작업시간을 갖도록 할 수있다. ex) 파일전송기능이 있는 메신저 (다운로드<채팅)
void setPriority(int newPriority) //우선순위 변경
int getPriority() //우선순위 반환
//상수
public static final int MAX_PRIORITY = 10 //최대 우선순위
public static final int MIN_PRIORITY = 1 //최소
public static final int NORM_PRIORITY = 5 //보통
//숫자가 커질수록 우선순위가 높아짐
CPU 코어의 수 >= 쓰레드 수면 우선순위에 따른 차이가 없지만
CPU 코어의 수 < 쓰레드 수 가 되면 우선순위의 차이가 있다. (OS마다 다름)
또한, 쓰레드에 우선순위를 부여하는 대신 작업에 우선순위를 두어 PriorityQueue에 저장해 놓고, 우선 순위가 높은 작업이 처리되도록 하는 것이 나을 수 있다.
쓰레드 그룹
서로 관련된 쓰레드를 그룹으로 다루기 위한 것, 그룹 in 그룹도 가능
자신이 속하거나 하위 쓰레드 그룹은 변경할 수 있지만 다른 그룹은 변경할 수 없다.
생성자 / 메서드 | 설명 |
ThreadGroup (String name) | 지정된 이름의 새로운 쓰레드 그룹을 생성 |
ThreadGroup (ThreadGroup Parent, String name) | 지정된 쓰레드 그룹에 포함되는 새로운 쓰레드 그룹 생성 |
int activeCount() | 쓰레드 그룹에 포함된 활성상태에 있는 쓰레드 수를 반환 |
int activeGroupCount() | 쓰레드 그룹에 포함된 활성상태에 있는 쓰레드 그룹의 수를 반환 |
void checkAccess() | 현재 실행중인 쓰레드가 쓰레드 그룹을 변경할 권한이 있는지 체크, 권한이 없다면 SecurityExceprion을 발생 |
void destory() | 쓰레드 그룹과 하위 쓰레드 그룹까지 모두 삭제한다. 단, 쓰레드 그룹과 하위 그룹이 비어있어야한다. |
int enumerate(Thread[] list) int enumerate(Thread[] list, boolean recurse) int enumerate(ThreadGroup[] list) int enumerate(ThreadGroup[] list, boolean recurse) |
쓰레드 그룹에 속한 쓰레드, 하위 쓰레드 그룹의 목록을 지정된 배열에 담고 그 개수를 반환. recurse의 값을 true로 하면 속한 하위 쓰레드 그룹의 쓰레드와 쓰레드 그룹까지 배열에 담는다. |
int getMaxPriority() | 쓰레드 그룹의 최대우선순위를 반환 |
void setMaxPriority(int pri) | 쓰레드 그룹의 최대우선순위를 설정 |
String getName() | 쓰레드 그룹의 이름을 반환 |
ThreadGroup getParent() | 쓰레드 그룹의 상위 쓰레드그룹을 반환 |
void interrupt() | 쓰레드에 그룹에 속한 모두 쓰레드를 interrupt |
void setDaemon(boolean daemon) | 쓰레드 그룹을 데몬 쓰레드그룹으로 설정(T)/해제(F) |
boolean isDaemon() | 쓰레드 그룹이 데몬 쓰레드그룹인지 확인 |
boolean isDestroyed() | 쓰레드 그룹이 삭제되었는지 확인 |
void list() | 쓰레드 그룹에 속한 쓰레드와 하위 쓰레드그룹에 대한 정보 출력 |
boolean parentOf(ThreadGroup g) | 지종된 쓰레드 그룹의 상위 쓰레드그룹인지 확인 |
ThreadGroup getThreadGroup() | 자신이 속한 쓰레드 그룹을 반환 |
void uncaughtException (Thread t, Throwable e) |
쓰레드 그룹의 쓰레드가 처리되지 않은 예외에 의해 종료되었을 때, JVM에 의해 이 메서드가 자동적으로 호출된다. |
그룹 포함 시키기
Thread (ThreadGroup group, String name)
Thread (ThreadGroup group, Runnable taget)
Thread (ThreadGroup group, Runnable taget, String name)
Thread (ThreadGroup group, Runnable taget, String name, long stackSize)
모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 한다. 그룹을 따로 지정하지 않으면 디폴트로 자신을 생성한 쓰레드와 같은 그룹에 속하게 된다.
어플리케이션이 실행되면, JVM은 main과 system이라는 쓰레드 그룹을 만들고 JVM운영에 필요한 쓰레드들을 생성해서 이 쓰레드 그룹에 포함시킨다. main쓰레드는 main쓰레드 그룹, 가비지컬렉션을 수행하는 Finalizer쓰레드는 system쓰레드 그룹에 속한다.
우리가 생성하는 모든 쓰레드 그룹은 main쓰레드 그룹의 하위 쓰레드 그룹이 되고 그룹을 지정하지 않고 생성한 쓰레드는 main쓰레드 그룹에 속하게 된다.
class ThreadEx9{
public static void main(String[] args) throws Exception{
ThreadGroup main = Thread.currentThread().getThreadGroup();
ThreadGroup grp1 = new ThreadGroup("Group1");
ThreadGroup grp2 = new ThreadGroup("Group2");
// ThreadGroup(ThreadGroup prarent, String name);
ThreadGroup subGrp1 = new ThreadGroup(grp1,"SubGroup1");
grp1.setMaxPriority(3); //Group1의 최대 우선순위를 3으로 변경.
Runnable r = new Runnable(){
public void run(){
try {
Thread.sleep(1000); //1초간 멈춤
} catch(InterruptedException e) { }
}
};
//Thread(ThreadGroup tg, Runnable r, String name);
new Thread(grp1, r, "th1").start();
new Thread(subGrp1, r, "th2").start();
new Thread(grp2, r, "th3").start();
System.out.println(">>List of ThreadGroup : " + main.getName()
+", Active ThreadGroup : " + main.activeGroupCount()
+", Active Thread: " + main.activeCount());
main.list();
}
}
}
===========
실 행 결 과
===========
>>List of ThreadGroup : main, Active ThreadGroup : 3, Active Thread: 5
java.lang.ThreadGroup[name=main,maxpri=10]
Thread[main,5,main]
java.lang.ThreadGroup[name=Group1,maxpri=3]
Thread[th1,3,Group1]
java.lang.ThreadGroup[name=SubGroup1,maxpri=3]
Thread[th2,3,SubGroup1]
java.lang.ThreadGroup[name=Group2,maxpri=10]
Thread[th3,5,Group2]
데몬 쓰레드
다른 일반 쓰레드의 작업을 돕는 보조적인 역할을 수행하는 쓰레드이다. 일반 쓰레드가 종료되면 데몬 쓰레드는 강제적으로 자동 종료되는데, 그 이유는 일반 쓰레드의 보조역할을 수행하므로 존재의 의미가 없기 때문이다.
이 점을 제외하고는 데몬, 일반 쓰레드는 다르지 않다. 데몬 쓰레드의 예로는 가비지 컬렉터, 워드 자동저장 등이 있다.
데몬 쓰레드는 무한루프와 조건문을 이용해서 실행 후 대기하고 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성한다.
데몬 쓰레드는 일반 쓰레드와 작성방법과 실행방법이 같으며 실행하기 이전에 setDaemon(true)를 호출하기만 하면 된다. 그리고 데몬 쓰레드가 생성한 쓰레드는 자동적으로 데몬 쓰레드가 된다.
class ThreadEx10 implements Runnable {
static boolean autoSave = false;
public static void main(String[] args){
Thread t = new Thread(new ThreadEx10());
t.setDaemon(true); // 이 부분이 없으면 종료되지 않는다.
t.start();
for(int i=0; i <= 10; i++){
try{
Thread.sleep(1000);
} catch(InterruptedException e) { }
System.out.println(i);
if(i==5)
autoSave = true;
}
System.out.println("종료");
}
public void run() {
while (true) {
try{
Thread.sleep(3*1000); //3초마다
} catch(InterruptedException e) { }
//autoSave의 값이 true면 autoSave 호출
if(autoSave) {
autoSave();
}
}
}
public void autoSave(){
System.out.println("자동 저장 완료");
}
}
===========
실 행 결 과
===========
0
1
2
3
4
5
6
7
자동 저장 완료
8
9
10
종료
import java.util.*;
class ThreadEx11{
public static void main(String[] args) {
ThreadEx11_1 t1 = new ThreadEx11_1("Thread1");
ThreadEx11_2 t2 = new ThreadEx11_2("Thread2");
t1.start();
t2.start();
}
}
class ThreadEx11_1 extends Thread {
ThreadEx11_1(String name) {
super(name);
}
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
class ThreadEx11_2 extends Thread {
ThreadEx11_2(String name) {
super(name);
}
public void run() {
Map map = getAllStackTraces();
Iterator it = map.keySet().iterator();
int x = 0;
while (it.hasNext()) {
Object obj = it.next();
Thread t = (Thread) obj;
StackTraceElement[] ste = (StackTraceElement[]) (map.get(obj));
System.out.println("[" + ++x + "] name : " + t.getName()
+ ", group : " + t.getThreadGroup().getName()
+ ", daemon : " + t.isDaemon());
for (int i = 0; i < ste.length; i++) {
System.out.println(ste[i]);
}
System.out.println();
}
}
}
===========
실 행 결 과
===========
[1] name : Reference Handler, group : system, daemon : true
java.base@11.0.20/java.lang.ref.Reference.waitForReferencePendingList(Native Method)
java.base@11.0.20/java.lang.ref.Reference.processPendingReferences(Reference.java:241)
java.base@11.0.20/java.lang.ref.Reference$ReferenceHandler.run(Reference.java:213)
[2] name : Signal Dispatcher, group : system, daemon : true
[3] name : Thread1, group : main, daemon : false
java.base@11.0.20/java.lang.Thread.sleep(Native Method)
app//ThreadEx11_1.run(Test.java:18)
[4] name : Attach Listener, group : system, daemon : true
[5] name : Finalizer, group : system, daemon : true
java.base@11.0.20/java.lang.Object.wait(Native Method)
java.base@11.0.20/java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:155)
java.base@11.0.20/java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:176)
java.base@11.0.20/java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:170)
[6] name : Monitor Ctrl-Break, group : main, daemon : true
java.base@11.0.20/java.net.InetSocketAddress.<init>(InetSocketAddress.java:224)
java.base@11.0.20/java.net.Socket.<init>(Socket.java:230)
app//com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:50)
[7] name : Common-Cleaner, group : InnocuousThreadGroup, daemon : true
java.base@11.0.20/java.lang.Object.wait(Native Method)
java.base@11.0.20/java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:155)
java.base@11.0.20/jdk.internal.ref.CleanerImpl.run(CleanerImpl.java:148)
java.base@11.0.20/java.lang.Thread.run(Thread.java:834)
java.base@11.0.20/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:134)
[8] name : Thread2, group : main, daemon : false
java.base@11.0.20/java.lang.Thread.dumpThreads(Native Method)
java.base@11.0.20/java.lang.Thread.getAllStackTraces(Thread.java:1657)
app//ThreadEx11_2.run(Test.java:30)
getAllStackTraces()를 이용하면 실행 중 또는 대기상태, 즉 작업이 완료되지 않은 모든 쓰레드의 호출스택을 출력한다.
결과를 보면 호출되었을때, 새로 생성한 1,2를 포함해서 모두 8개의 쓰레드가 실행 중 또는 대기 상태에 있다는 것을 알 수 있다.
프로그램을 실행하면, JVM은 가비지컬렉션, 이벤트처리, 그래픽처리와 같이 필요한 데몬 쓰레드들을 자동 생성해서 실행시킨다. 그리고 이들은 'system쓰레드 그룹' 또는 'main쓰레드 그룹'에 속한다.
AWT나 Swing과 같이 GUI를 가진 프로그램은 더 많은 수의 데몬 스레드가 생성된다.