[CH12. 쓰레드] 프로세스와 쓰레드

1. 프로세스와 쓰레드

프로세스(process)는 간단하게 말하면 실행중인 프로그램이다.
프로그램 –실행–> 프로세스
프로세스는 데이터, 메모리등의 자원과 쓰레드로 구성되어있음.
프로세스의 자원을 이용해서 실제 작업을 수행하는 것.
모든 프로세스는 최소 하나 이상의 쓰레드가 존재. 둘 이상이면 멀티쓰레드 프로세스라한다.

  • 멀티쓰레딩의 장점
    • CPU의 사용률을 향상시킴.
    • 자원을 보다 효울적으로 사용할 수 있음.
    • 사용자에 대한 응답성이 향상됨.
    • 작업이 분리되어 코드가 간결해짐.

동기화(synhronization), 교착상태(deadlock)등을 고려해서 신중히 프로그래밍 해야함.

2. 쓰레드의 구현과 실행

구현 방법

  1. Thread클래스 상속

    1
    2
    3
    4
    5
    class MyThread extends Thread{
    public void run(){
    //Thread의 run()함수를 오버라이딩.
    }
    }
  2. Runnable인터페이스 구현(일반적인 방법)

    1
    2
    3
    4
    5
    class MyThread implements Runnable{
    public void run(){
    //추상메서드 run()을 구현
    }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ThreadEx1{
public static void main(String[] args){
ThreadEx1_1 t1 = new ThreadEx1_1();

Runnable r = new ThreadEx1_2();
Thread t2 = new Thread(r);
}
}

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++) {
System.out.println(Thread.currentThread().getName());
}
}
}

쓰레드 생성 후 start()를 호출해야 작업을 시작함.
한번 사용한 쓰레드는 다시 재사용할 수 없다. 하나의 쓰레드에 한번의 start()만 호출 될 수 있음.

1
2
3
4
5
6
7
8
9
10
11
ThreadEx1_1 t1 = new ThreadEx_1();
t1.start();
t.start();//이건 불가능
```


```java
ThreadEx1_1 t1 = new ThreadEx1_1();
t1.start();
t1 = new ThreadEx1_1();
t.start();//이건 가능

3. start()와 run()

  • run()을 호출하는 것은 생성된 쓰레드를 실행하는 것이 아니라 단순히 클래스에속한 메서드를 하나 호출하는것.

call stack

run

main

  • start()을 호출하는 것은 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택을 생성한 후 run()을 호출해서 생성된 호출스택에 run()이 저장되게 한다.
    모든 쓰레드는 독립적인 작업을 수행하기 위해 자신만의 호출스택을 필요로 하기 때문에 새로운 쓰레드를 생성하고 실행시킬때마다 새로운 호출스택이 생성되고 쓰레드가 종료되면 작업에 사용된 호출스택은 소멸된다.
  1. main메서드에서 쓰레드의 start메서드를 호출한다.
  2. start메서드는 쓰레드가 작업을 수행하는데 사용될 새로운 호출 스택을 생성한다.
  3. 생성된 호출스택에 run 메서드를 호출해서 쓰레드가 작업을 수행하도록 한다.
  4. 이제는 호츨스택이 2개이기때문에 스케줄러가 정한 순서에 으해 번갈아 가면서 실행된다.

실행중인 쓰레드가 하나도 없을때 프로그램은 종료된다.

4. 싱글쓰레드와 멀티쓰레드

두개의 작업을 하나의 쓰레드로 하면 한 작업 끝난 후 다른 작업 끝.
두개의 작업을 두개의 쓰레드로 하면 짧은시간동안 쓰레드 2개가 번갈아 가면서 작업을 수행해서 동시에 두 작업이 처리되는것과 같다고 느낌.
CPU만 사용하는 계산 작업이면 멀티쓰레드가 전환하는 시간때문에 오히려 느림.
CPU외 자원을 사용하는 경우 싱글쓰레드 프로세스 보다 멀티쓰레드프로세스가 더 효율적임.
ex)외부기기에서 입출력 받는 경우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import javax.swing.JOptionPane;

class ThreadEx6{
public static void main(String[] args) throws Exception{
String input = JOptionPanel.showInputDialog("아무 값이나 입력하세요.");
System.out.println("입력하신 값은 "+input+"입니다.");

for(int i = 10; i > 0 ; i--) {
System.out.println(i);
try{
Thread.sleep(1000);
}catch(Exception e){}
}
}
}

위의 예는 입력을 받은 후 출력하기 때문에 사용자가 입력하는 동안에는 출력 작업이 일어나지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import javax.swing.JOptionPane;

class ThreadEx7{
public static void main(String[] args) throws Exception{

ThreadEx7_1 th1 = new ThreadEx7_1();
th1.start();

String input = JOptionPanel.showInputDialog("아무 값이나 입력하세요.");
System.out.println("입력하신 값은 "+input+"입니다.");

}
}
class ThreadEx7_1 extends Thread{
public void run(){
for(int i = 10; i > 0 ; i--) {
System.out.println(i);
try{
Thread.sleep(1000);
}catch(Exception e){}

}
}
}

위의 코드는 입력받는 동안 출력되고 입력된 값이 출력되고 또 출력이 된다.

5. 쓰레드의 우선순위

쓰레드에 우선순위(priority)의 멤버변수가 있다.
우선순위의 범위는 1~10이고 숫자가 높을수록 더 우선순위가 높다.
우선순위의 값은 상대적이다. 1,2와 8,9의 결과 값이 같다.
우선순위는 쓰레드생성한 쓰레드로부터 상속받는다.
main 메서드를 수행하는 쓰레드의 우선순위는 5.
파일 다운로드와 채팅기능 중 채팅에 더 우선순위를 높여야 한다.

1
2
3
4
5
6
void setPripority(int new Priority);
int getPriority();

public static final int MAX_PRIORITY =10;
public static final int MIN_PRIORITY =1;
public static final int NORM_PRIORITY = 5;

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
class ThreadEx9{
public static void main(String[] args) throws Exception{

ThreadEx9_1 th1 = new ThreadEx9_1();
ThreadEx9_2 th2 = new ThreadEx9_2();
th2.setPriority(7);
System.out.println("입력하신 값은 "+input+"입니다.");
th1.start();
th2.start();
}
}

class ThreadEx9_1{
public void run(){
for(int i = 0; i < 300 ; i++) {
System.out.println("-");
for(int j = 0; j < 1000000 ; j++) {}
}
}
}
class ThreadEx9_2{
public void run(){
for(int i = 0; i < 300 ; i++) {
System.out.println("|");
for(int j = 0; j < 1000000 ; j++) {}
}
}
}

위 결과는 |가 먼저 끝나고, - 이 완료된다.

6. 쓰레드 그룹(thread group)

서로 관련된 쓰레드를 그룹으로 다루기 위한것.
폴더를 생성해서 관련된 파일을 묶어 관리하는것처럼 쓰레드도 그룹으로 묶어서 관리.
쓰레드를 쓰레드 그룹에 포함시키려면 Thread생성자를 이용해야함

1
2
Thread(ThreadGroup group, String name)
Thread(ThreadGroup gorup, Runnable target)

7. 데몬쓰레드(deamon thread)

데몬쓰레드는 일반 쓰레드의 작업을 돕는 보조역할.
일반쓰레드 작업이끝나면 데몬 쓰레드는 강제종료됨.
ex) 가비지컬렉터, 위드프로세서 자동저장, 화면자동갱신
무한루프와 조건문을 이용해서 실행 후 대기하고 있다가 특정 조건이 되면 작업 수행하고 다시 대기.

8. 쓰레드의 실행제어

쓰레드 프로그램이 어렵게 느껴지는 건 동기화와 스케줄링때문임.

  • 쓰레드 스케쥴링과 관련된 메서드
생성자/메서드 설명
void interupt() sleep()이나 join()에 의해 일시정지상태인 쓰레드를 실행대기상태로 만든다.
해당 쓰레드에서는 interuptedExcetption이 발생함으로써 일시정지상태를 벗어나게 된다.
void join()
void join(long millis)
void join(long millis, int nanos)
지정된 시간동안 쓰레드가 실행되도록한다. 지정된 시간이 자나거나 작업이 종료되면 join()을 호출한 쓰레드로 다시 돌아와 실행을 계속한다.
void resume() supend()에 의해 일시정지상태에 있는 쓰레드를 실행대기 상태로 만든다.
static void sleep(long millis)
static void sleep(long millis, int nanos)
지정된 시간( 천분의 일초 단위) 동안 쓰레드를 일시정지 시킨다. 지정한 시간이 지나고 나면 자동적으로 다시 실행대기가 된다.
void stop() 쓰레드를 즉시 종료시킨다. 교착상태(dead-lock)에 빠지기 쉽기 때문에 deprecated되었다.
void suspend() 쓰레드를 일시정지시킨다. resume()을 호출하면 다시 실행대기상태가 된다.
satic void yield() 실행중에 다른 쓰레드에게 양보하고 실행대기상태가 된다.
  • 쓰레드의 상태
상태 설명
NEW 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태
RUNNABLE 실행 중 또는 실행 가능한 상태
BLOCKED 동기화 블럭에 의해서 일시정지된 상태(Lock이 풀릴때까지 기다리는 상태)
WATING,
TIMED_WATING
쓰레드의 작업이 종료되지는 않았지만 실행가능하지 않은 일시정지 상태. TIMED_WATEING은 일시정지시간이 지정된 경우를 의미한다.
TERMINATED 쓰레드의 작업이 종료된 상태
  1. 쓰레드 생성하고 start()을 호출하면 실행대기열에 저장. 순서를 기다림. Quequ와같은 구조로 먼저 실행대기열에 들어온 쓰레드가 먼저 실행됨.
  2. 실행대기상태이다가 자기 차례가 오면 실행상태가 됨.
  3. 주어진 실행시간이 다되거나 yeild()를 만나면 다시 실행대기 상태가 되고 다음 차례의 쓰레드가 실행상태가 됨.
  4. 실행중 suspend(), sleep(), wait(), join(), I/O block에 의해 일시정지상태가 딜 수 있음.
  5. 지정된 일시정지 시간이 지나거나 notify(), reusme(), interrup()가 호출되면 일시정지 상태를 벗어나 다시 실행대기열에 들어가 순서를 기다림.
  6. 실행을 모두 마치거나 stop()이 호출되면 쓰레드는 소멸됨.
  • join()을 사용한 예시

    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
    62
    63
    64
    65
    66
    67
    class ThreadEx13{
    static long startTime=0;
    public static void main(String[] args) throws Exception{

    ThreadEx13_1 th1 = new ThreadEx13_1();
    ThreadEx13_2 th2 = new ThreadEx13_2();
    th1.start();
    th2.start();
    startTime = System.currentTimeMillis();

    try{
    th1.join();//th1의 작업이 끝날때까지 기다린다.
    th2.join();//th2의 작업이 끝날때까지 기다린다.
    }catch (InterruptedException e){}

    System.out.println("소요시간:" + (System.currentTimeMillis()- ThreadEx13.startTime));
    }
    }
    class ThreadEx13_1{
    public void run(){
    for(int i = 0; i < 300 ; i++) {
    System.out.println("-");
    }
    }//run()
    }
    class ThreadEx13_2{
    public void run(){
    for(int i = 0; i < 300 ; i++) {
    System.out.println("|");
    }
    } //run()
    }
    ```

    join()을 사용하지 않으면 main 쓰레드는 바로 종료되지만, join()을 사용해서 th1과 th2의 작업이 마칠때까지 main쓰레드가 기다림
    - 쓰레드가 순차적으로 실행되어야 할때 사용하는 예제.

    ```java
    class ThreadEx14{
    static long startTime=0;
    public static void main(String[] args) throws Exception{

    ThreadEx14_1 th1 = new ThreadEx14_1();
    ThreadEx14_2 th2 = new ThreadEx14_2();
    th1.start();

    try{
    th1.join();
    }catch (InterruptedException e){}
    th2.start();
    }
    }

    class ThreadEx14_1{
    public void run(){
    for(int i = 0; i < 300 ; i++) {
    System.out.println("-");
    }
    }//run()
    }
    class ThreadEx14_2{
    public void run(){
    for(int i = 0; i < 300 ; i++) {
    System.out.println("|");
    }
    }
    }
  • 아래 코드는 th1, th2, Main쓰레드 순으로 종료됨

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
class ThreadEx15{
static long startTime=0;
public static void main(String[] args) throws Exception{

ThreadEx15_1 th1 = new ThreadEx15_1();
ThreadEx15_2 th2 = new ThreadEx15_2();
th1.start();
th2.start();
try{
th1.sleep();
}catch (InterruptedException e){}
System.out.println("<<main종료>>");
}
}

class ThreadEx15_1{
public void run(){
for(int i = 0; i < 300 ; i++) {
System.out.println("-");
}
System.out.println("<<TH1종료>>");
}//run()
}
class ThreadEx15_2{
public void run(){
for(int i = 0; i < 300 ; i++) {
System.out.println("|");
}
System.out.println("<<TH2종료>>");
} //run()
}
```

왜 th1이 sleep()으로 잠들어있어도 가장 먼저 종료될까?
sleep()이 항상 현재 실행중인 쓰레드에 대해 작동해서 th1.sleep()호출해도 main메서드를 실행하는 main쓰레드가 영향받는다.
static으로 선언되어 있어서 참조변수로 sleep()을 호출하기 보다는 Thread.sleep()이렇게 호츨해야 함.

## 9. 쓰레드의 동기화
멀티쓰레드는 여러 쓰레드가 같은 프로세스내의 자원을 공유하기 때문에 데이터가 원래 의도했더것과는 다르게 변경 될 수 있음.

### 9.1 synchorized를 이용한 동기화
공유 데이터에 lock을 걸어 먼저 작업중이던 쓰레드가 작업을 완전히 마칠때까지는 다른 쓰레드에게 제어권이 넘어가도 데이터가 변경되지 않도록 보호함.

- synchronized 사용방법 두가지.
가능하면 메서드에 synchronized를 사용하는 메서드 단위 동기화를 권장함.

```java
// 1. 특정한 객체에 lock을 걸고자 할때
sysnchronized(객체의 참조변수){

}
// 2. 메서드에 lock을 걸고자 할때
public void synchronized void calcSum(){

}

synchronized를 이용해서 객체를 동기화 하면 쓰레드가 교착상태에 빠질 수 있다.
교착상태란 구 쓰레드가 lock이 된 상태로 서로 lock가 풀리기를 무한정 기다리게 되는상황.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ThreadEx21 {
public static void main(String[] args){
RunnbaleImpl r = new RunnableImpl();
Thread th1 = new Thread(r);
Thread th2 = new Thread(r);
th1.start();
th2.start();
}
}

class RunnableTmpl implements Runnable {
int iv = 0;
public void run(){
int lv = 0;
String name = Thread.currentThread().getName();

while(lv<3){
System.out.println(name+"Local var: "+ ++lv);
System.out.println(name+"Instance var: "+ ++iv);
System.out.println();
}
}
}

실행결과

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Thread-0 Local var: 1
Thread-0 Instance var: 1

Thread-0 Local var: 2
Thread-0 Instance var: 2

Thread-0 Local var: 3
Thread-0 Instance var: 3

Thread-0 Local var: 1
Thread-0 Instance var: 4

Thread-0 Local var: 2
Thread-0 Instance var: 5

Thread-0 Local var: 3
Thread-0 Instance var: 6

여기서 인스턴스변수 iv는 main, th1, th2 쓰레드 모두 접근이 가능함.(쓰레드간의 변수 공유)
lv는 지역변수라 각 쓰레드 스택내에서 생성되어 공유되지 않는다.

  • 다음은 동기화가 잘 되지 않아 데이터 값이 변형된 예제.
    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
    class ThreadEx24{
    public static void main(String[] args){
    RunnbaleImpl r = new RunnableImpl();
    Thread th1 = new Thread(r);
    Thread th2 = new Thread(r);
    th1.start();
    th2.start();
    }
    }
    class Account{
    int balance =1000;
    public void withdraw (int money){
    if(balance>=money){
    try{ Thread.sleep(1000);} catch(Exception e){}
    balance -=money;
    }
    }//withdraw
    }
    class RunnableEx24 implements Runnable{
    Account acc = new Account();

    public void run(){
    while(acc.balance > 0){
    //100, 200, 300중의 한 값을 임의로 선택해서 출금
    int money = (int)(Math.random()*3+1)*100;
    acc.withdraw(money);
    System.out.println("balance:"+acc.balance);
    }
    }//run()
    }

실행 결과

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
balance:700
balance:400
balance:200
balance:0
balance:-100
```

잔고(balance)가 임의의 출금금액(money)보다 클 경우에만 출금하도록 되어있는데 잔고에 -100이 나왔음.
if조건문 통과하고 출금직전에 다른쓰레드가 끼어들어서 먼저 출금했기때문이다.
if문과 출금하는 기능은 하나로 synchronized되어야 한다.

```java
public synchronized void withdraw (int money){
if(balance>=money){
try{ Thread.sleep(1000);} catch(Exception e){}
balance -=money;
}
}//withdraw
```

```java
public void withdraw (int money){
synchronized(this){
if(balance>=money){
try{ Thread.sleep(1000);} catch(Exception e){}
balance -=money;
}

}
}//withdraw

9.2 wait()과 notify()

쓰레드를 동기화 할때 효율을 높이기 위해 사용할 수 있다.
한쓰레드가 lock걸려 다른 쓰레드는 lock이 풀릴때까지 기다려야 되는 상황이 있음.
쓰레드에 lock을 걸는것 대신에 wait()을 호출해서 다른 쓰레드에 제어권을 넘겨주고 대기상태로 기다리다가 다른쓰레드에 의해 notify()가 호출되면 다시 실행상태가 되도록 함
wait()과 notify()는 Object클래스에서 정의되서 모든 객체에서 호출이 가능함.
동기화 블록 내에서만 사용가능. 쓰레드가 wait()을 호출하면 그때까지 걸어 놓은 lock을 풀고 대기실에 들어가기 됨.
notify()는 객체의 wating pool에 있는 쓰레드 중 하나만 깨움.

  • wait(), notify(), notifyAll()
    • Object에 정의 되어있다.
    • 동기화 블록(synchronized)내에서만 사용할 수 있다.
    • 보다 효율적인 동기화를 가능하게 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Account{
int balance =1000;
public synchronized void withdraw (int money){
if(balance>=money){
try{
wait();
} catch(Exception e){}
balance -=money;
}
}//withdraw
}

public synchronized void desposit(int money){
balance += money;
notify();
}

댓글

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×