쓰레드란?
프로세스란 운영체제(OS)가 메모리에 올려 사용중인 프로그램을 말한다. 이 프로세스에서도 프로그램이 흘러갈수 있게 로직의 흐름이 필요한데 이것을 쓰레드라 한다. 프로세스는 하나의 쓰레드만 갖을수도 여러개의 쓰레드를 갖을수도 있다. 싱글 쓰레드 프로그램, 멀티 쓰레드 프로그램이라 한다.
멀티쓰레드를 쓰는이유?
게임(프로세스)안에서도 음악이 나오면서 공격이펙트를 보여준다거나 캐릭터가 걸어가는 모습을 랜더링하면서 백단으로는 데이터를 갖고 오거나 한번에 동시에 일을 병렬처리할때 사용된다.
Java 쓰레드의 사용법
자바에서는 쓰레드를 사용할때 Thread 클레스를 상속받거나 Runable인터페이스를 구현하여 사용한다.
package study;
public class MyThreadTest {
public static void main(String[] args) {
// 방법1. Thread 상속
MyThread01 th1 = new MyThread01();
// 방법2. Runable 구현
MyThread02 run1 = new MyThread02();
Thread th2 = new Thread(run1);
// 방법3. 익명클레스로 구현
Thread th3 = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0; i<100; i++) {
System.out.println("MyThread03....");
}
}
});
// 방법4. Lamnda로 구현
Thread th4 = new Thread(() -> {
for(int i=0; i<100; i++)
System.out.println("MyThread04....");
});
// 쓰레드 시작
th1.start();
th2.start();
th3.start();
th4.start();
}
}
class MyThread01 extends Thread {
@Override
public void run() {
for(int i=0; i<100; i++){
System.out.println("MyThread01...");
}
}
}
class MyThread02 implements Runnable {
@Override
public void run() {
for(int i=0; i<100; i++){
System.out.println("MyThread02...");
}
}
}
Start() 함수와 Run() 함수
start() 함수
- 새로운 **쓰레드(실행 흐름)**를 생성해서 run() 메서드를 호출함.
- 즉, 실제 멀티쓰레딩이 일어남.
- Thread 클래스나 Runnable 인터페이스를 구현한 객체를 실행할 때 사용함.
run() 함수
- start() 없이 run()을 직접 호출하면, 새로운 쓰레드를 만들지 않고, 현재 실행 중인 쓰레드에서 run() 함수가 실행됨.
- 멀티쓰레딩이 아님. 그냥 일반 메서드 호출과 같음.
package step01;
/**
* Run과 Start
* Run -> 그냥 메서드 실행
* Start -> 다른 쓰레드를 만들어 run메서드 실행
*/
public class ThreadEx2 {
public static void main(String[] args) {
ThreadEx2_1 t = new ThreadEx2_1();
t.start(); // main, run -> throwException
t.run(); // main -> run -> throwException
}
}
class ThreadEx2_1 extends Thread {
@Override
public void run() {
throwException();
}
public void throwException(){
try {
throw new Exception();
} catch (Exception e) {
e.printStackTrace();
}
}
}
싱글쓰레드와 멀티쓰레드
기본적으로 자바로 실행한 프로그램은 main 쓰레드가 무조건 하나 할당된다. 싱글쓰레드로 작업을 처리하는 것과 멀티 쓰레드로 작업을 처리하는 것이 어떠한 것인지 확인해 보자
싱글 쓰레드로 작업 처리
package step02;
public class ThreadEx4 {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
for(int i=0; i<500; i++)
System.out.printf("%s", new String("-"));
System.out.print("소요시간1: " + (System.currentTimeMillis() - startTime));
for(int i=0; i<500; i++)
System.out.printf("%s", new String("|"));
System.out.print("소요시간2: " + (System.currentTimeMillis() - startTime));
}
}
멀티 쓰레드로 작업 처리
package step02;
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<500; i++)
System.out.printf("%s", new String("-"));
System.out.print("소요시간1:" + (System.currentTimeMillis() - ThreadEx5.startTime));
}
}
class ThreadEx5_1 extends Thread{
@Override
public void run() {
for(int i=0; i<500; i++)
System.out.printf("%s", new String("|"));
System.out.print("소요시간2:" + (System.currentTimeMillis() - ThreadEx5.startTime));
}
}
두개를 비교해보면 멀티쓰레드로 처리된게 더 느리다. 그것은 컨텍스트 스위치 시간 때문이다.
멀티쓰레드를 사용한다고 무조건 속도가 빨라지는 것은 아니다. 멀티쓰레드를 사용하기 적합할때 사용해야 효과가 있다.
어떨때 멀티쓰레드를 사용해야하는가?
IO작업이나 사용자의 응답을 기다리는경우(쓰레드가 블락되는경우) , 네트워크로 부터 데이터를 주고받을때, 프린터로 파일을 출력하는 작업과 같이 외부기기와 입출력하는 경우 그 작업만 하길 기다리는 것보다 멀티쓰레드로 병렬처리를 하는 것이 좋다.
package step03;
import javax.swing.*;
public class ThreadEx6 {
public static void main(String[] args) {
String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
System.out.println("입력하신 값은 " + input + "입니다.");
for(int i=10; i>0; i--) {
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
}
}
}
하나의 메인쓰레드로 부터 작업이 시작된다. 입력이 완료될때까지 기다리다 입력이 완료되어야 숫자를 세기 시작한다.
package step03;
import javax.swing.*;
public class ThreadEx7 {
public static void main(String[] args) {
ThreadEx7_1 th1 = new ThreadEx7_1();
th1.start();
String input = JOptionPane.showInputDialog("아무 값이나 입력주세요.");
System.out.println("입력하신 값은 " + input + "입니다.");
}
}
class ThreadEx7_1 extends Thread{
@Override
public void run() {
for(int i=10; i>0; i--){
System.out.println(i);
try{
sleep(1000);
}catch(Exception e) {}
}
}
}
이전 예제와 달리 입력을 기다리면서 다른 쓰레드로는 숫자를 세기 시작한다.
쓰레드 우선순위
우선순위에 값에 따라 실행시간을 더 많이 할당 받을수 있다. 1 ~ 10사이의 숫자를 부여해 우선순위를 매길수 있다. 메인쓰레드는 기본적으로 5로 되어 있다. 메인쓰레드에서 쓰레드를 생성하면 기본적으로 5를 상속받는다.
package step04;
/**
* 쓰레드 우선 순위
*/
public class ThreadEx8 {
public static void main(String[] args) {
ThreadEx8_1 th1 = new ThreadEx8_1();
ThreadEx8_2 th2 = new ThreadEx8_2();
th2.setPriority(7);
System.out.println("Priority of th1(-) : " + th1.getPriority());
System.out.println("Priority of th2(|) : " + th2.getPriority());
th1.start();
th2.start();
}
}
class ThreadEx8_1 extends Thread{
@Override
public void run() {
for(int i=0; i<300; i++){
System.out.print("-");
for(int x=0; x<10000000; x++);
}
}
}
class ThreadEx8_2 extends Thread {
@Override
public void run() {
for(int i=0; i<300; i++){
System.out.print("|");
for(int x=0; x<10000000; x++);
}
}
}
쓰레드의 우선순의를 조절하여 예제를 작성 하였다.
쓰레드 그룹
폴더 처럼 쓰레드 그룹을 만들 수 있다.
주요 메서드와 생성자는 아래와 같다
생성자 / 메서드 설명
───────────────────────────────────────────────────────────────
ThreadGroup(String name) 지정된 이름의 새로운 쓰레드 그룹을 생성한다.
ThreadGroup(ThreadGroup parent, String name) 지정된 쓰레드 그룹에 포함되는 새로운 쓰레드 그룹을 생성한다.
int activeCount() 쓰레드 그룹에 포함된 활성상태에 있는 쓰레드의 수를 반환한다.
int activeGroupCount() 쓰레드 그룹에 포함된 활성상태에 있는 쓰레드 그룹의 수를 반환한다.
void checkAccess() 현재 실행중인 쓰레드가 쓰레드 그룹을 변경할 권한이 있는지 체크한다. 만일 권한이 없다면 SecurityException을 발생시킨다.
void destroy() 쓰레드 그룹과 하위 쓰레드 그룹까지 모두 삭제한다. 단, 쓰레드 그룹이나 하위 쓰레드 그룹이 비어있어야 한다.
int enumerate(Thread[] list)
int enumerate(Thread[] list, boolean recurse) 쓰레드 그룹에 속한 쓰레드 또는 하위 쓰레드 그룹의 목록을 지정된 배열에 담고 그 개수를 반환한다.
두 번째 매개변수인 recurse의 값을 true로 하면 쓰레드 그룹에 속한 하위 쓰레드 그룹에 쓰레드 또는 쓰레드 그룹까지 배열에 담는다.
int enumerate(ThreadGroup[] list)
int enumerate(ThreadGroup[] list, boolean recurse)
int getMaxPriority() 쓰레드 그룹의 최대우선순위를 반환한다.
String getName() 쓰레드 그룹의 이름을 반환한다.
ThreadGroup getParent() 쓰레드 그룹의 상위 쓰레드 그룹을 반환한다.
void interrupt() 쓰레드 그룹에 속한 모든 쓰레드를 interrupt한다.
boolean isDaemon() 쓰레드 그룹이 데몬 쓰레드 그룹인지 확인한다.
boolean isDestroyed() 쓰레드 그룹이 삭제되었는지 확인한다.
void list() 쓰레드 그룹에 속한 쓰레드와 하위 쓰레드 그룹에 대한 정보를 출력한다.
boolean parentOf(ThreadGroup g) 지정된 쓰레드 그룹의 상위 쓰레드 그룹인지 확인한다.
void setDaemon(boolean daemon) 쓰레드 그룹을 데몬 쓰레드 그룹으로 설정/해제한다.
void setMaxPriority(int pri) 쓰레드 그룹의 최대우선순위를 설정한다.
package step05;
/**
* 쓰레드 그룹
*
* ThreadGroup getThreadGroup() - 쓰레드 자신이 속한 쓰레드 그룹을 반환한다.
* void uncaughtException(Thread t, Throwable e) - 쓰레드 그룹의 쓰레드가, 처리되지 않은 예외에
* 의해 실행이 종료되었을 때, JVM에 의해 이 메서드가 자동적으로 호출된다.
*/
public class ThreadEx9 {
public static void main(String[] args) {
ThreadGroup main = Thread.currentThread().getThreadGroup();
ThreadGroup grp1 = new ThreadGroup("Group1");
ThreadGroup grp2 = new ThreadGroup("Group2");
// ThreadGroup(ThreadGroup parent, String name)
ThreadGroup subGrp1 = new ThreadGroup(grp1, "SubGroup1");
grp1.setMaxPriority(3); // 쓰레드 그룹 grp1의 최대우선순위를 3으로 변경.
Runnable r = new Runnable() {
@Override
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();
}
}
데몬 쓰레드
쓰레드를 돕는 보조 쓰레드로 부모쓰레드가 종료되면 같이 종료 된다.
package step06;
/**
* 데몬 쓰레드
* boolean isDeamon() - 쓰레드가 데몬 쓰레드인지 확인한다. 데몬 쓰레드이면 true를 반환한다.
* void setDeamon(boolean on) - 쓰레드를 데몬 쓰레드로 또는 사용자 쓰레드로 변경한다.
* 매개변수 on의 값을 true로 지정하면 데몬 쓰레드가 된다.
*
*/
public 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("프로그램을 종료합니다.");
}
@Override
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("작업파일이 자동 저장 되었습니다.");
}
}
getAllStackTraces()
package step06;
import java.util.Iterator;
import java.util.Map;
public 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);
}
@Override
public void run() {
try {
sleep(5 * 1000); // 5초 동안 기다린다.
} catch(InterruptedException e) { }
}
}
class ThreadEx11_2 extends Thread {
ThreadEx11_2 (String name) {
super(name);
}
@Override
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();
}
}
}
getAllStackTraces()를 이용하면 실행 중 또는 대기상태, 즉 작업이 완료되지 않은 모든 쓰레드의 호출스택을 출력 할 수 있다. 결과를 보면 getAllStackTraces()가 호출되었을때, 새로 생성한 Thread1, Thread2를 포함해서 모두 6개의 쓰레드가 실행 중 또는 대기상태에 있다는 것을 알 수 있다.
프록르매을 실행하면, JVM은 가비지컬렉션, 이벤트 처리, 그래픽처리와 같이 프로그램이 실행되는 데 필요한 보조작업을 수행하는 데몬 쓰레드들을 자동적으로 생성해서 실행시킨다. 그리고 이 들은 'system쓰레드 그룹' 또는 'main 쓰레드 그룹'에 속한다.
AWT나 Swing과 같이 GUI를 가진 프로그램을 실행하는 경우에는 이벤트와 그래픽처리를 위해 더 많은 수의 데몬 쓰레드가 생성된다.
워커 쓰레드(Worker Thread)란?
워커 쓰레드는 "일을 시켜놓으면 대기하고 있다가 꺼내서 처리하는" 노동자 쓰레드라고 보면 됨.
🏭 예시 비유: 공장 시스템
- 메인 쓰레드: 주문을 받고 작업을 큐에 넣음
- 작업 큐: 해야 할 일들(작업 요청)이 쌓여 있음
- 워커 쓰레드: 줄 서 있는 작업을 하나씩 꺼내서 처리하는 일꾼
실제 구조
[ Main Thread ]
|
V
+-------------------+
| 작업 큐 (Queue) |
+-------------------+
↑ ↑ ↑
| | |
Worker1 Worker2 Worker3 ... (Worker Thread Pool)
특징 정리
목적 | 반복적으로 들어오는 작업을 효율적으로 처리 |
쓰레드 개수 | 일반적으로 고정된 수의 워커가 계속 돌고 있음 |
장점 | 쓰레드 생성/종료 오버헤드 줄이고 성능 향상 |
활용 |
문제
ThreadEx.java
package step01;
public class ThreadEx {
public static MyQueue<Task> workQueue = new MyQueue<Task>();
public static void main(String[] args) {
// 작업큐 생성
for(int i=0; i<10; i++) {
workQueue.enqueue(new MyTask(i + " 번째 Task"));
}
// 워커 쓰레드 10개 만들어서 시작
// 구현...
}
}
// 구현...
class Worker implements Runnable {
@Override
public void run() {
}
}
interface Task {
void execute();
}
class MyTask implements Task {
private String taskName;
public MyTask(String taskName) {
this.taskName = taskName;
}
@Override
public void execute() {
System.out.println(taskName + " 처리중....");
try {
Thread.sleep(1000 * 10); // 10초걸리는작업...
} catch (InterruptedException e) { }
}
}
MyArrayList.java
import java.util.ArrayList;
public class MyArrayList<T> {
public static final int STEP_NEXT = 2;
private int size;
private int capacity;
private Object[] arr;
public MyArrayList() {
this.size = 0;
this.capacity = 10;
arr = new Object[capacity];
}
public boolean add(T data){
boolean result = false;
if(size >= capacity){
capacity *= STEP_NEXT;
Object[] newArr = new Object[capacity];
for(int i=0; i<size; i++){
newArr[i] = arr[i];
}
arr = newArr;
}
arr[size++] = data;
return result;
}
public T get(int index){
return (T) arr[index];
}
public int size(){
return size;
}
public int capacity(){
return capacity;
}
public boolean remove(int index){
if(index < 0 || index > size - 1){
return false;
}
arr[index] = null;
for(int i=index; i<size-1; i++){
arr[i] = arr[i+1];
}
size--;
return true;
}
@Override
public String toString() {
String result = "";
for(int i=0; i<size; i++){
result += arr[i] + " ";
}
return result;
}
}
MyQueue.java
public class MyQueue<T> {
private MyArrayList<T> arr;
public MyQueue() {
arr = new MyArrayList<T>();
}
// 담을수 있는 용량
// 용량을 초과한다면 ?? => 용량을 2배로 늘려야한다
public int capacity() {
}
// 현재 담겨져 있는 싸이즈
public int size() {
}
// 큐에 맨 처음에 데이터 추가
public void enqueue(T item) {
}
// 큐에 맨 처음값 가져오기
// 호출후에도 갯수는 변함 없다.
public T peek() {
}
// 큐에 맨 처음값 가져오기
// 호출하면 맨처음에 값은 사라진다.
public T dequeue() {
}
// 큐:: 선입선출(First In First Out)
// 큐에 몇번째 위치에 있는지?
// 제일 먼저 들어간 값이 위치가 1이다
// 제일 나중에 들어간 값이 제일 높다
// 못찾으면 -1 리턴
public int search(T item) {
return -1; // 못찾음
}
// 값을 볼수 있게 toString 구현
@Override
public String toString() {
}
}
답안