안녕, 세상!

9. 다중 쓰레드 본문

It공부/Java

9. 다중 쓰레드

dev_Lumin 2020. 7. 16. 07:04

(1) 쓰레드 (Thread) 기본

쓰레드는 자바에서 제공되는 강력한 기능입니다.

자바에서는 쓰레드 처리를 프로그래밍 언어 수준에서 자유자재로 사용할 수 있기 때문에 프로그래머는 더 이상 복잡한 하드웨어 수준의 처리를 할 필요가 없습니다.

 

 

① 다중 프로세스(Multi-Process) 시스템

CPU가 프로그램을 읽으면 프로그램에서 명령한대로 메모리를 할당하고 순서대로 실행하게 됩니다.

이렇게, 컴퓨터 메모리 위에서 실행되고 있는 프로그램을 프로세스(Process)라고 합니다.

 

하나의 프로세스만 실행되는 싱글 프로세스 시스템에서는 사용자로부터 입력을 받는다거나 인쇄를 하는 동안에는 컴퓨터 CPU는 놀고 있을 수 밖에 없습니다.

이를 보완하기 위해서 CPU를 사용하지 않을 때 다른 프로세스를 실행시킬 수 있는 다중 프로세스 시스템을 사용해서 여러 개의 프로세스를 정해진 순서대로 돌아가면서 시분할하여 CPU를 조금씩 사용하게 됩니다.

예를 들어, 하나의 프로그램을  여러 번 실행시키면  여러 개의 프로세스로 메모리에 적재되어 실행되는 것입니다.

프로세스는 자신만의 실행환경과 메모리 공간을 가지는데, 이것이 쓰레드와 프로세스가 다른점이라고 할 수 있습니다.

 

하나의 프로세스를 얼마동안 실행시킬지, 다음에 어떤 프로세스를 실행시킬지 등을 결정하는 일을 스케줄(Scheduling) 이라고 합니다.

 

 

② 다중 쓰레드(Muti-Thread) 시스템

다중 프로세스 시스템에서는 여러 개의 프로세스가 하나의 컴퓨터 안에서 동시에 수행되지만, 하나의 프로세스는 한 가지 일만 할 수 있습니다.

하지만 시간이 지날수록 하나의 프로세스가 여러가지 일을 할 필요가 생겼습니다.

하나의 프로세스가 여러 개의 작업을 동시에 할 때, 각각의 작업쓰레드(Thread)라고 하며, 여러 개의 쓰레드를 동시에 실행시킬 수 있는 시스템을 다중 쓰레드 시스템이라고 합니다.

쓰레드는 프로세스와 같은 실행환경을 가지고, 프로세스 안에 존재하여 메모리와 파일을 등 프로세스 자원을 공유합니다.

 

정리하자면, 프로세스와 쓰레드의 차이는 프로세스는 운영체제로 부터 자원을 할당 받는 작업의 단위고, 쓰레드는 할당 받은 자원을 이용하는 실행의 단위입니다.

 

 

 

 

(2) 쓰레드 생성 및 사용

자바에서 쓰레드를 생성하고 사용하는 방법

(1) Thread  클래스를 상속받아 사용하기

 - 간판한 방법이지만 자바에서 다중 상속이 불가능하므로 이미 다른 클래스를 상속받은 클래스의 경우 사용불가

(2) Runnable 인터페이스를 상속받아 사용하기

 

 

① Thread 클래스 사용

Thread 클래스를 상속받아 쓰레드를 생성하는 방법은 가장 일반적이고 쉬운 방법입니다.

다음 표는 Thread 클래스의 주요 메소드입니다.

 

Thread 클래스를 상속받아서 이용하여 쓰레드를 생성하고 실행하는 형식은 다음과 같습니다.

 

 

class myThread extends Thread {        // myThread : Thread 클래스 상속받은 클래스명

    public void run()

    {

            //쓰레드에서 수행할 작업들;

            ....

    }

}

            ....

    myThread 객체 = new myThread();

    객체.start();

            ....

 

 

 start() 메소드가 호출되면 자동으로 쓰레드가 생성되고, 생성된 쓰레드는 자동으로 run() 메소드를 실행합니다.

run() 메소드에 실제 쓰레드가 실행할 작업들이 있고, run() 메소드가 종료되면 쓰레드는 자동적으로 소멸됩니다.

보통 run() 메소드의 작업을 반복문이나 무한루프를 사용합니다.

무한 루프에 빠지더라도 null 값을 대입하면 언제든지 제거할 수 있습니다.

 

다음은 쓰레드를 사용한 예시로 쓰레드 두 개가 1초마다 정수 0부터 시작해서 증가하는 숫자를 계속 출력하는 코드입니다.

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
class myThread extends Thread{
    int n=0;
    public myThread(String name) {  //생성자
        super(name);   //Thread 클래스 생성자에 인수 반환
    }                  //super은 myThread의 부모인 Thread 클래스
    public void run()
    {
        while(true) {
            System.out.println(this.getName() + "->"+n);
            n++;
            try {
                sleep(1000);  //1초 동안 쓰레드 중지
            }catch(InterruptedException ie) {} // 예외처리 
        }
    }
}
 
public class thdsample1 {
    public static void main(String []args) {
        myThread my1,my2;
        
        my1 = new myThread("첫 번째 쓰레드");
        my2 = new myThread("두 번째 쓰레드");
        my1.start(); // 쓰레드 생성및 run() 자동실행
        my2.start();
    }
}
cs

무한 루프로 계속 반복됩니다.

 

 

 

 

② Runnable 인터페이스 사용

자바에서 클래스는 여러 클래스로부터 다중 상속이 불가능해서 다른 클래스를 상속받은 클래스는 Thread 클래스를 상속 받지 못하으므로 다중 상속이 가능한 인터페이스를 이용해서 쓰레드를 구현할 수 있습니다.

Runnable 인터페이스 사용형식은 다음과 같습니다.

 

 

class myRun implements Runnable     // myRun : Runnable 인터페이스를 상속받은 클래스명

{

    public void run()

    {

        // 쓰레드에서 수행할 작업들

        .... 

    }

}

    myRun 객체1 = new myRun();

    Thread 객체2 = new Thread(객체1);

    객체2.start();

 

 

Runnable 인터페이스로 구현한 클래스의 객체(객체1)를 인수로 주어 Thread 클래스의 객체(객체2)를 생성합니다.

그 후 Thread 객체(객체2)의 start() 메소드를 호출해야 합니다.

Thread 클래스의 생성자는 Runnable 인터페이스의 서브 클래스(객체1을 말하는 것임)를 인수로 받으면, 해당 클래스의 run() 메소드를 실행하는 쓰레드를 만들게 됩니다.


다음은 Runnable 인터페이스를 이용하여 생성한 쓰레드에서 현재 날짜와 시간을 윈도우에 출력하는 예제입니다.

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
import javax.swing.*;
import java.awt.*;
import java.util.Calendar;   // 현재 날짜 시간 가져오기 위해서
class myRun implements Runnable {
    JLabel lb1, lb2;
    public myRun(JLabel lb1, JLabel lb2) {  // myRun의 생성자()
        this.lb1=lb1;   
        this.lb2=lb2;     //this.lb1,2 는 이 클래스의 lb1,2입니다.
    }
    public void run() {
        int year, month, date, hour, min, sec;
        String s1, s2;
        Calendar c;
        
        while(true) {   // 무한 루프를해서 시간이 계속 갱신됨
            c= Calendar.getInstance(); 
            
            year=c.get(Calendar.YEAR);
            month=c.get(Calendar.MONTH)+1;
            date=c.get(Calendar.DATE);
            hour=c.get(Calendar.HOUR);
            min=c.get(Calendar.MINUTE);
            sec=c.get(Calendar.SECOND);
            
            s1 = "오늘은"+year+"년"+month+"월"+date+"일입니다.";
            s2 = "현재시간: "+hour+"시"+min+"분"+sec+"초";
                    
            lb1.setText(s1);
            lb2.setText(s2);
            
            try {
                Thread.sleep(1000);   // 시간이 1초마다 갱신됨(1초동안 쓰레드 중지이므로)
            }catch(InterruptedException ie) { return;}
        }
    }
}
 
public class calendarsample extends JFrame{
    public calendarsample() {
        setTitle("시간");
        setLayout(new FlowLayout());
        
        JLabel lb1 = new JLabel();
        JLabel lb2 = new JLabel();
        
        myRun r = new myRun(lb1,lb2);  // lb1,lb2의 주소가 넘어간다고 생각하면 됨
        Thread my = new Thread(r);
        
        add(lb1);
        add(lb2);
        
        setSize(200,100);
        setVisible(true);
        
        my.start();
    }
    public static void main(String [] args) {
        /*calendarsample cs= */ new calendarsample();
    }
}
cs

 

결과가 현재시간에 맞게 계속 시간이 흐릅니다.

 

 

 

(3) 쓰레드의 생명주기, 스케줄링

① 생명주기(Life cycle)

쓰레드가 생성되면 쓰레드의 상태는 다음과 같이 실행, 대기, 준비, 종료 등의 상태를 거칠 수 있습니다.

(1) 실행(Run)

생성된 스레드는 start() 메소드로 실행할 수 있고, 쓰레드가 실행되면 run() 메소드 내의 명령들이 차례대로 실행됩니다.

일반적으로 사용자는 Thread 클래스를 상속받아 run() 메소드를 오버라이딩 해서 새롭게 run()을 작성합니다.

 

(2) 대기

쓰레드가 실행을 멈추고 다음번에 실행되기를 기다리고 있는 상태입니다.

상태 명령 설명
Sleep sleep() 주어진 시간동안 아무것도 안하고 기다리는 상태
Wait wait() notify() 혹은 notifyAll() 명령이 내려질 때까지 동기화 블록 내에서 기다리는 상태
Block   키보드나 마우스, 네트워크로부터의 입출력 이벤트가 끝날 때까지 기다리는 상태

 

(3) 준비(Ready)

쓰레드가 실행 상태에 들어가기 위해 준비하고 있는 상태입니다.

쓰레드가 실행 상태로 바로 들어가려고 해도, 먼저 실행하고 있는 쓰레드가 있을 수 있기 때문에 쓰레드 스케줄러가 선택한 순서대로 쓰레드가 실행됩니다.

 

(4) 종료

쓰레드가 실행은 멈추고 정지되는 상태입니다.

쓰레드가 더 이상 사용되지 않으면 자바 가상머신은 쓰레드를 정지시키고 종료 상태로 바꿉니다.

 

 

 

 

② 쓰레드 스케줄링(Thread Scheduling)

여러개의 쓰레드를 생성하여 동시에 실행하게 되면 각 쓰레드는 일정한 동일한 순위로 실행됩니다.

자바의 쓰레드 스케줄러가 공평하게 각 쓰레드를 선택하여 실행하게 됩니다.

만약 쓰레드마다 우선순위를 다르게 지정하면, 스케줄러는 우선순위가 높은 쓰레드를 먼저 선택하여 실행하게 되고 우선순위가 높은 쓰레드가 더 자주 먼저 실행됩니다.

 

자바에서 setPrioirity() 메소드를 사용하면 각 쓰레드의 우선순위를 지정할 수 있습니다.

setPriority는 1~10 의 정수형 인수를 사용하며, 숫자가 높을수록 우선순위가 높습니다.

 

 

Thread 객체 = new Thread();

객체.setPriority(우선순위값);

 

 

우선순위값을 숫자가 아닌 미리 정의한 상수를 사용할수도 있습니다.

우선순위 상수 설명
MAX_PRIORITY 최대 우선순위(10)
NORMAL_PRIORITY 보통 우선순위(5)
MIN_PRIORITY 최소 우선순위(1)

 

다음은 쓰레드의 우선순위를 부여하여 실행시킨 예제입니다.

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
class mthread extends Thread {
    public mthread(String name) {
        super(name);
    }
    public void run() {
        try {
            for(int n=0;n<10;n++) {
                sleep(1);
                System.out.println(this.getName()+": "+n);
            }
            System.out.println(this.getName()+"실행종료");
        }catch(InterruptedException ie) { return;}
    }
}
public class prioritysample{
    public static void main(String args[]) {
        mthread mt1,mt2;
        mt1 = new mthread("최대 우선순위 쓰레드");
        mt1.setPriority(10);
        mt2 = new mthread("최소 우선순위 쓰레드");
        mt2.setPriority(1);
        mt1.start();
        mt2.start();
    }
}
cs

최대 우선순위 쓰레드가 최소 우선순위 쓰레드보다 먼저 실행이 종료된 것을 확인할 수 있습니다.

 

 

 

 

 

(4) 쓰레드의 동기화 (Synchronization)

동기화란 여러 개의 쓰레드가 동시에 실행되면서 하나의 같은 자원을 사용하려고 할 때, 어느 한 시점에서는 하나의 쓰레드만을 사용하도록 하는 것입니다.

 

예시를 들어서 동기화에 대해서 설명하겠습니다.

쓰레드 A, B, C 세 개의 쓰레드는 지정된 작업을 수행하면서 작업 횟수를 count 변수 값을 1씩 증가시키면서 저장합니다.

현재 Count변수에는 3이라는 정수가 저장되어 있고 세 개의 쓰레드가 공유하는 하나의 같은 자원입니다.

쓰레드 A가 현재 값인 3에서 1을 더하여 count의 값을 4로 만들려고 합니다.

쓰레드 A가 4로 만들려는 직전(순간)에 쓰레드 B와 C도 count 변수의 값인 3을 읽고 3에서 1을 더해서 count 변수값을 4로 만드려고 합니다.

(예시 순서: A가 count를 4로만듬 -> B도 count값을 4로 만듬 -> C도 count 값을 4로 만듬)

그러므로 3개의 쓰레드가 작업한 값인 Count 변수가 6이 되는 것이 아니라 1만 증가하는 4가 됩니다.

작업횟수는 A,B,C 각각 1회식으로 총 3회가 증가하여야 하는데 1만 증가하니까 문제가 생깁니다.

 

따라서 count 변수에 값을 저장할 때에는 하나의 쓰레드에서만 그 값을 읽고 변경할 때 까지 다른 쓰레드에서는 접근하면 안됩니다.

이러한 변수 count와 같은 영역을 임계(Critical Section) 이라고 합니다.

 

 

자바에서 임계영역을 지정하기 위해서 메소드 앞에 synchronized 키워드를 사용해야 합니다.

쓰레드는 synchronized로 선언된 메소드를 실행하기 전에 lock을 얻고, synchronized로 선언된 메소드가 끝나면 lock을 반환합니다.

만일 그 사이에 다른 쓰레드가 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
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
import java.util.Random;
 
class mask{
    static int mask_count = 20;
    
    static public synchronized void TakeOut(String name,int n) {
        if(n<=mask_count) {
            mask_count -= n;
            System.out.print(name+": "+n+"개의 마스크를 구매함");
        }
        else
        {
            System.out.println("원하는 마스크 개수가 모자랍니다.");
        }
        System.out.println(">> 남은 마스크: "+mask_count);
    }
}
class myThread3 extends Thread{
    public myThread3(String name) {
        super(name);
    }
    public void run() {
        Random r =new Random();
        for(int i=0;i<10;i++) {   // 한 쓰레드 당 10번 안에 끝낼거 같아서 10번 설정함
            try {
                sleep(100);
            }catch(InterruptedException ie) { return;}
            if(mask.mask_count==0) {
                System.out.println("마스크가 모두 팔렸습니다.");
                return;
            }
            mask.TakeOut(this.getName(), Math.abs(r.nextInt() % 4)); // Math.abs()는 절댓값 반환
        }
    }
}
public class synchrosample {
    public static void main(String args[]) {
        myThread3 mt1, mt2, mt3;
        
        mt1 = new myThread3("이지은");
        mt2 = new myThread3("김찬미");
        mt3 = new myThread3("장다혜");
        
        mt1.start();
        mt2.start();
        mt3.start();
    }
}
cs

처음에 총 마스크 개수는 20개고 사람 당 최대 3개를 살 수 있도록 random 값을 설정했습니다.

'마스크가 모두 팔렸습니다.' 라는 결과 값이 3개 나온것은 쓰레드가 3개라서 그런 것입니다.

 

 

 

 

wait(), notify(), notifyAll()

앞에 다뤘던 synchronized 키워드는 여러 개의 쓰레드가 동시에 임계영역에 들어가지 못하도록 할 때 사용됩니다.

반면 두 개의 쓰레드를 교대로 임계영역에 들어가게 할 때는 wait() 메소드를 사용하여 쓰레드를 대기시키고, notify() 메소드를 사용하여 쓰레드를 재구동시키는 방법이 훨씬 효과적입니다.

 

① wait() 메소드

쓰레드가 임계영역에서 wait() 메소드를 만나면 가지고 있던 lock을 양보하고 대기 상태로 들어갑니다.

대기 상태로 들어간 쓰레드는 다른 쓰레드가 notify() 혹은 notifyAll() 메소드로 통지할 때 까지 기다립니다.

wait() 메소드는 한 쓰레드에만 적용되며, synchronized 키워드로 선언된 임계영역에서만 호출될 수 있습니다.

wait() 에 대한 대기 조건은 지속적으로 검사되어야 하기 때문에 while() 문 등의 반복문에서 사용되며, 조건문에서 사용할 수 없습니다.

 

 

② notify() 메소드, notifyAll() 메소드

notify() 메소드가 호출되면, wait() 메소드에 의해 대기중인 쓰레드가 다시 재 구동됩니다.

notify() 메소드 역시 synchronized 키워드로 선언한 임계영역에서만 호출될 수 있습니다.

즉, notify() 메소드를 호출하는 쓰레드는 lock을 가지고 있어야 합니다.

notify() 메소드는 대기 중인  쓰레드가 여러 개 일 경우 그 중 하나만 깨우고,

notifyAll() 메소드는 대기 중인 모든 쓰레드를 깨웁니다.

notify() 메소드가 여러 쓰레드 중 하나를 깨울 때, 어느 쓰레드를 깨울지는 자바 가상머신이 결정합니다.

 

 

다음은 생산자와 소비자 프로그램을 2개 쓰레드로 구현하고, 이를 wait() 메소드와 notifyAll() 메소드를 이용하여 번갈아 가며 생산하고 소비하는 기능을 구현한 것입니다.

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
68
69
70
71
72
import java.util.Random;
class Buffer {
    int data=0;
    boolean available = false//현재 버퍼에 값이 없다는 뜻 
    
    public synchronized int get() {  // 소비자가 이용하는 동기화 메소드
        while(available ==false) {
            try {
                wait();
            }catch(InterruptedException ie) { }
        }
        available=false// 소비자가 값을 가져가게되면 이 코드를 거칠테니  값이 없다는 뜻인 false로 만들어줌
        notifyAll();    // put메소드에서 wait() 당하는 생산자를 풀어줌
        return data;
    }
    
    public synchronized void put(int num) { // 생산자가 이용하는 값을 넣는 메소드
        while(available==true) { // 버퍼에 이미 값이 존재한다는 뜻
            try {
                wait();
            }catch(InterruptedException ie) {return;}
        }
        data=num;  // 생산자가 버퍼에 데이터를 넣음
        available = true;  // 생산자가 값을 넣어주고 이 코드를 거치니 값이 넣어졌다고 true라고 표기
        notifyAll();  // get메소드에서 wait() 당하는 소비자를 풀어줌
    }
}
class Producer extends Thread { // 생산자 클래스
    Buffer dataBuffer;
    
    public Producer(Buffer b) { // 생성자
        dataBuffer= b;
    }
    public void run() {
        Random r =new Random();
        
        for(int i=0;i<10;i++) {
            dataBuffer.put(i);
            System.out.println("생산자: "+i); // 출력되면 생산자가 i값을 버퍼에 넣었다는 뜻
            try {
                sleep(Math.abs(r.nextInt()%100));
            }catch(InterruptedException ie) { }
        }
    }
}
class Consumer extends Thread {
    Buffer dataBuffer;
    
    public Consumer(Buffer b) {
        dataBuffer =b;
    }
    public void run() {
Random r =new Random();
        
        for(int i=0;i<10;i++) {
            dataBuffer.get();
            System.out.println("소비자: "+i); // 출력되면 소비자가 i값을 버퍼에서 가져왔다는 의미
            try {
                sleep(Math.abs(r.nextInt()%100));
            }catch(InterruptedException ie) { }
        }
    }
}
public class prodcons {
    public static void main(String [] args) {
        Buffer b = new Buffer();    // 메인 함수에 Buffer 객체를 하나 만
        Producer p = new Producer(b);  // 생산자 메소드와
        p.start();
        Consumer c = new Consumer(b);  // 소비자 메소드가 하나의 객체를 공유해서 실행해야되기 때문이다.
        c.start();
    }
}
cs

 

 

이 예제는 하나의 데이터만을 저장할 수 있는 버퍼(그렇다고 가정)를 사용하여 두 개의 쓰레드가 번갈아 가며 데이터를 쓰고, 읽는 예제입니다.

소비자 쓰레드는 생산자 쓰레드가 데이터를 버퍼에 저장하지 않으면 데이터를 가져갈 수 없고 wait() 메소드를 통해 대기상태로 들어가게 됩니다.

생산자 쓰레드는 데이터를 버퍼에 저장하고 nofityAll() 메소드를 이용하여 소비자 쓰레드를 깨웁니다.

소비자 쓰레드는 생산자 쓰레드가 버퍼에 출력한 데이터를 get() 메소드를 이용하여 가져갈 수 있습니다.

 

반대의 경우도 마찬가지로 생산자 쓰레드는 소비자 쓰레드가 버퍼에서 데이터를 가져가지 않았으면 데이터를 버퍼에 저장할 수 없고 wait90 메소드를 통해 대기 상태로 들어가게 됩니다.

소비자 쓰레드는 데이터를 버퍼에서 가져가고 생산자 쓰레드를 notifyAll() 메소드를 이용하여 깨웁니다.

생산자 쓰레드는 put() 메소드를 이용하여 버퍼에 데이터를 저장할 수 있습니다.

 

 

'It공부 > Java' 카테고리의 다른 글

11. 네트워크 프로그래밍  (0) 2020.07.18
10. 스트림과 파일 처리  (0) 2020.07.17
8. 그래픽  (0) 2020.07.15
7. 이벤트 프로그래밍 (2)  (0) 2020.07.14
7. 이벤트 프로그래밍 (1)  (0) 2020.07.13
Comments