Java Thread

Thread란 프로그램 실행 시 프로세스 내부에 존재하는 수행 단위를 말한다.

Java에서는 두 가지의 Thread의 구현 방법이 있다.

  • Thread를 상속받아서 사용하는 방법
    • 이 경우 다른 Class의 상속이 불가능하다.
  • Runnable interface를 구현하는 방법
    • 일반적인 방법이다.
    • 다른 Class의 상속이 가능하다.

Thread 생성

Thread 는 상태 변환을 통해 아래 그림과 같은 Lifecycle을 갖는다.

Thread lifecycle

동작상태설명
객체 생성NEWThread 객체의 생성 / start() 호출 전
실행 대기RUNNABLE실행 상태로 언제든 갈 수 있는 상태
일시정지WAITING다른 Thread가 통지할 때까지 기다리는 상태
TIMES_WAITING주어진 시간동안 기다리는 상태
BLOCKED사용하고자 하는 객체의 Lock이 해제될 때까지 기다리는 상태
종료TERMINATED실행을 마친 상태

Thread를 상속받아 사용하는 방법

  • Thread로 수행될 Task class를 생성한다.
1
2
3
4
5
6
7
8
9
10
11
12
public class Task extends Thread {
String mName;
Task(String str) {
mName = str;
}
public void run() { // Thread 시작 시 수행된다.
System.out.println("run " + mName);
}
}
  • Thread를 만들어 실행한다.

start()가 호출되면 새로운 Thread가 작업을 실행하는데 필요한 Call Stack을 생성하고 run()을 호출하여 Stack에 저장한다.

run() 메소드는 Thread scheduler에게 전달된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) {
Task tA = new Task("a");
Task tB = new Task("b");
Task tC = new Task("c");
tA.start(); // start()를 호출하면 앞에서 만든 Task의 run()이 실행된다.
tB.start();
tC.start();
}
}

Runnable interface를 구현하여 사용하는 방법

  • Thread로 돌아갈 class를 생성한다.
1
2
3
4
5
6
7
8
9
10
11
12
public class Task implements Runnable {
String mName;
Task(String str) {
mName = str;
}
public void run() {
System.out.println("run " + mName);
}
}
  • 새 Thread 객체를 생성하여 start() 한다.

run() 으로 실행하면 단순히 Task 객체의 run() 메소드가 수행되는 것 뿐이다. start()로 해야 별도의 Thread가 생성되어 수행된다.

Thread가 제대로 생성되어 수행되는지는 Eclipse의 debug 모드 등에서 threadStatus 등 thread 관련 항목들이 제대로 수행되는지 확인한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Main {
public static void main(String[] args) {
Task tA = new Task("a");
Task tB = new Task("b");
Thread t1 = new Thread(tA);
Thread t2 = new Thread(tB);
Thread t3 = new Thread(new Task("c"));
t1.start();
t2.start();
t3.start();
}
}

아래와 같이 구현도 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
public void run() {
System.out.println("run");
}
});
t.start();
}
}

join()

Thread의 수행 시간이 오래 걸릴 때, Thread보다 Main 함수가 먼저 종료되는 경우가 있다.

이런 경우 join() 메소드를 사용하면 모든 Thread들이 종료된 후 Main 함수가 종료된다.

예를 들어, 아래 코드를 실행한 결과를 보면 fin이 먼저 호출되는 것을 볼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Main {
public static void main(String[] args) {
Task tA = new Task("a");
Task tB = new Task("b");
Thread t1 = new Thread(tA);
Thread t2 = new Thread(tB);
Thread t3 = new Thread(new Task("c"));
Thread t4 = new Thread(new Task("d"));
Thread t5 = new Thread(new Task("e"));
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
System.out.println("fin");
}
}
// 결과
run a
run c
fin
run d
run e
run b

다음과 같이 join()을 사용할 경우, Thread들이 모두 실행된 이후에 fin이 찍힌다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class Main {
public static void main(String[] args) {
Task tA = new Task("a");
Task tB = new Task("b");
Thread t1 = new Thread(tA);
Thread t2 = new Thread(tB);
Thread t3 = new Thread(new Task("c"));
Thread t4 = new Thread(new Task("d"));
Thread t5 = new Thread(new Task("e"));
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
try {
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("fin");
}
}
// 결과
run a
run d
run e
run b
run c
fin

Synchronized

Thread가 함수를 통해 전역변수에 변형을 가할 경우, 각 Thread가 돌면서 값이 잘못 변하는 경우가 있다.

만약 아래와 같은 코드가 있다고 하면 (여기서는 10개지만 더 많이…) 마지막에 결과값이 10이 안 찍히고 9가 찍히는 것을 볼 수 있다.

물론 실행할 때마다 값이 다르다.

내가 만들어서 그런지 예시가 거지같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Task implements Runnable {
String mName;
static int no = 0;
Task(String str) {
mName = str;
}
public static void inc() {
no++;
}
public void run() {
try {
Thread.sleep(1000);
inc();
} catch(Exception e) {
}
System.out.println("run " + mName);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class Main {
public static void main(String[] args) {
Task tA = new Task("a");
Task tB = new Task("b");
Thread t1 = new Thread(tA);
Thread t2 = new Thread(tB);
Thread t3 = new Thread(new Task("c"));
Thread t4 = new Thread(new Task("d"));
Thread t5 = new Thread(new Task("e"));
Thread t6 = new Thread(new Task("f"));
Thread t7 = new Thread(new Task("g"));
Thread t8 = new Thread(new Task("h"));
Thread t9 = new Thread(new Task("i"));
Thread t10 = new Thread(new Task("j"));
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
t7.start();
t8.start();
t9.start();
t10.start();
try {
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
t6.join();
t7.join();
t8.join();
t9.join();
t10.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(tA.no);
System.out.println("fin");
}
}
// 결과
run a no: 0
run d no: 7
run c no: 0
run i no: 6
run j no: 5
run e no: 3
run f no: 4
run g no: 2
run b no: 1
run h no: 8
9
fin

위의 문제가 발생하는 이유는 예를 들면 하나의 Thread가 no를 3에서 4로 증가시키고 있을 때 다른 Thread가 접근하고, 그 Thread도 3에서 4로 증가시키게 된다.

그러면 inc()가 두 번 수행되었더라도 no는 4에 머물러있게 된다.

이러한 문제를 방지하기 위해 no++ 과정을 synchronized(동기화) 시켜주면 된다.

하나의 Thread가 synchronized 키워드 안의 내용을 수행 중이라면 다른 Thread는 그 자원에 접근할 수 없게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Task implements Runnable {
String mName;
static int no = 0;
Task(String str) {
mName = str;
}
synchronized public static void inc() {
no++;
}
public void run() {
try {
Thread.sleep(1000);
inc();
} catch(Exception e) {
}
System.out.println("run " + mName);
}
}

Thread Pool

Thread를 생성하기 위해서는 시간과 메모리가 소요된다. Java는 JVM(Java Virtual Machine) 위에서 돌아가고 JVM은 Thread의 생성 개수를 제한하지 않는다.

때문에 Thread를 과도하게 생성한다면 성능 저하는 물론 Memory leak이 발생한다.

Thread의 무분별한 생성을 막기 위해 쓰레드 관리 방식인 Thread Pool을 사용한다.

Thread Pool은 Thread를 허용된 개수 안에서 사용할 수 있도록 제약한다. 이 제약은 JVM이 하는 것이 아니라 어플리케이션에서 해야 한다.

JDK 1.5 이전에는 개발자가 직접 만들어서 사용했으며, 1.5부터 java.util.concurrent 를 통해 지원하게 되었다.

  • Excutors.newFixedThreadPool(n)
    • 최대 Thread 수가 n 개인 Pool.
    • 동시에 일어나는 업무량이 비교적 일정할 때 사용한다.
  • Excutors.newCachedThreadPool()
    • Thread 수의 제한을 두지 않는다. 새로운 Thread 시작 요청이 들어올 때마다 Thread를 하나씩 생성한다.
    • 수행이 종료된 Thread들이 바로 사라지지 않고 1분동안 살아있다가 다른 작업 요청이 없다면 사라지게 된다.
    • 짧고 반복되는 작업에 사용한다.
  • Executors.newSingleThreadExecutor()
    • 하나의 Thread를 생성한다.
    • 주로 Thread 작업 중 예외가 발생한 경우 예외 처리를 위한 Thread 생성 용으로 사용한다.

선언은 ExecutorService 로 한다.

Excutors.newFixedThreadPool(n)

1
2
3
4
5
6
7
8
9
ExecutorService executorService = Executors.newFixedThreadPool(2);
// Thread 생성 요청.
// Task는 수행할 class 명.
// 세 번 요청하면 세 번째 Thread는 앞의 두 개 중 하나가 종료될 때까지 수행되지 않는다.
executorService.execute(new Task("name"));
executorService.shutdown(); // 추가적인 Thread 요청을 거부한다.
while (!executorService.isTreminated()) { // 모든 Thread가 완료될 때까지 대기한다.
}

Excutors.newCachedThreadPool()

1
2
3
4
5
6
ExecutorService executorService = Executor.newCachedThreadPool();
executorService.execute(new Task("name"));
executorService.shutdown();
while (!executorService.isTreminated()) {
}

Executors.newSingleThreadExecutor()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(()->{
try {
Thread.sleep(1000);
} catch (Exception e) {
};
}
});
executorService.shutdown();
while (!executorService.isTreminated()) {
}

출처

Share