티스토리 뷰

프로세스(Process)와 쓰레드(Thread)

  • 프로세스'실행중인 프로그램(Program)'이다. 프로그램을 실행하면 OS로 부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 된다. 
  • 프로세스는 프로그램을 수행하는데 필요한 데이터와 메모리 등의 자원 그리고 쓰레드로 구성되어 있으며 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 바로 쓰레드이다. 
  • 모든 프로세스에는 최소한 하나 이상의 쓰레드가 존재하며, 둘 이상의 쓰레드를 가진 프로세스를 '멀티쓰레드 프로세스(Multi-threaded process)' 라고 한다.

쓰레드(Thread) 구현과 실행 

쓰레드를 생성하는 방법은 크게 두가지가 있다.

  • Thread 클래스를 사용
  • Runnable 인터페이스를 사용
  • Runnable 과 Thread 모두 java.lang 패키지에 포함되어있다. 

어느 쪽을 선택해도 별 차이는 없지만 Thread 클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문에, Runnable인터페이스를 구현하는 방법이 일반적이다. Runnable 인터페이스를 구현하는 방법은 재사용성이 높고 코드의 일관성을 유지할 수 있게 때문에 보다 객체지향적인 방법으로 볼 수 있다. 

[ Thread 클래스 ]

class MyThread extends Thread {
	@Override
	public void run () {  
    	// 작업내용
    }
}
public class Sample extends Thread {
    public void run() {  // Thread 를 상속하면 run 메서드를 구현해야 한다.
        System.out.println("thread 실행.");
    }

    public static void main(String[] args) {
        // Thread를 상속한 Sample 자체를 인스턴스화 하여 생성
        Sample sample = new Sample();
        sample.start();  // start()로 쓰레드를 실행한다.
    }
}
// 결과

thread 실행
  • Thread를 상속한 Sample은 바로 해당 클래스를 인스턴스화 하여 쓰레드를 생성하면된다. 
  • Sample 클래스가 Thread 클래스를 상속했다. Thread 클래스의 run 메소드를 구현하면 위와 같이 sample.start() 실행시 sample 객체의 run 메소드가 수행된다. 

[ Thread 동작 방식 ]

public class Sample extends Thread {
    int seq;

    public Sample(int seq) {
        this.seq = seq;
    }

    public void run() {
        System.out.println(this.seq + " thread 실행.");  // 쓰레드 시작
        try {
            Thread.sleep(1000);  // 1초 대기한다.
        } catch (Exception e) {
        }
        System.out.println(this.seq + " thread 종료.");  // 쓰레드 종료 
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {  // 총 10개의 쓰레드를 생성하여 실행한다.
            Thread t = new Sample(i);
            t.start();
        }
        System.out.println("main 종료.");  // main 메소드 종료
    }
}
// 결과

0 thread 실행
4 thread 실행
6 thread 실행
2 thread 실행
main 종료
3 thread 실행
7 thread 실행
8 thread 실행
1 thread 실행
9 thread 실행
5 thread 실행
0 thread 종료
4 thread 종료
2 thread 종료
6 thread 종료
7 thread 종료
3 thread 종료
8 thread 종료
9 thread 종료
1 thread 종료
5 thread 종료
  • 총 10개의 쓰레드를 실행시키는 예제이다. 어떤 쓰레드인지 확인하기 위해 쓰레드마다 생성자에 순번을 부여했다. 그리고 쓰레드 메소드(run) 수행시 시작과 종료를 출력하게 했다. 그리고 시작과 종료 사이에 1초의 간격이 생기도록 Thread.sleep(1000) 작성했다. 
  • main 메소드 종료시 "main end" 를 출력하도록 했다. 
  • 0번 쓰레드 부터 9번 쓰레드 까지 순서대로 실행이 되지 않고 그 순서가 일정하지 않은 것을 볼 수 있는데, 이를 통해 쓰레드는 순서에 상관없이 동시에 실행된다는 사실을 알 수 있다. 
  • main 메소드 종료 시 "main end" 라는 문자열이 출력되는데 위 결과를 보면 중간쯤에 출력되어 있음을 알 수 있다. 

[ Thread 실행 순서 제어 - Thread.join() ]

모든 쓰레드가 종료된 후에 main 메소드를 종료시키고 싶은 경우에는 Join() 메소드를 사용한다. 

import java.util.ArrayList;

public class Sample extends Thread {
	int seq;
	
	public Sample (int seq) {
		this.seq = seq;
	}
	public void run() { 
		System.out.println(this.seq + "thread 실행"); // thread 시작
		try {
			Thread.sleep(1000); // 1초 대기
		}catch (Exception e) {
			
		}
		System.out.println(this.seq + "thread 종료"); // thread 종료
	}
	public static void main(String[] args) {
		ArrayList<Thread> threads = new ArrayList<>();
		
		for(int i =0; i< 10; i++) {
			Thread t = new Sample(i);
			t.start();
			threads.add(t);
		}
		
		for(int i =0; i<threads.size(); i++) {
			Thread t = threads.get(i);
			// get(int index)은 인자로 인덱스를 받으며, 이 인덱스의 위치에 있는 객체를 리턴해준다.
			try {
				t.join(); // t 쓰레드가 종료될 때 까지 기다린다. 
			}catch(Exception e) {
				
			}
		}
		System.out.println("main end"); //메인 메소드 종료 

	}
}
// 결과

0thread 실행
4thread 실행
3thread 실행
1thread 실행
7thread 실행
2thread 실행
6thread 실행
5thread 실행
8thread 실행
9thread 실행
5thread 종료
8thread 종료
9thread 종료
2thread 종료
6thread 종료
1thread 종료
7thread 종료
0thread 종료
3thread 종료
4thread 종료
main end
  • 생성된 쓰레드를 담기 위해서 ArrayList 객체인 threads를 만든 후 쓰레드 생성 시 생성된 객체를 threads에 저장했다. 그리고 main 메소드가 종료되기 전에 threads에 담긴 각각의 thread에 join 메소드를 호출하여 쓰레드가 종료될 때까지 대기하도록 했다.
  • 쓰레드의 join() 메소드는 쓰레드가 종료될 때까지 기다리게 하는 메서드이다. 
  • "main end" 라는 문자열이 가장 마지막에 출력이 되었음을 확인할 수 있다. 
  • 쓰레드 프로그래밍시 가장 많이 하는 실수가 바로 쓰레드가 종료되지 않았는데 쓰레드가 종료된 줄 알고 그 다음 로직을 수행하게 만드는 일이다. 쓰레드가 종료된 후 그 다음 로직을 수행해야 할 때 join 메소드를 사용한다는 것을 기억하자. 

[ Runnable 인터페이스 ]

class MyThread implements Runnable {
	@ Override
	public void run () {  
    	// 작업내용
    }
}
public class MyThread implements Runnable {
	@Override
	public void run() {
		System.out.println("thread 실행");
	}
	public static void main(String[] args) {
		// Runnable 구현체인 MyThread를 쓰레드 생성자에 넣고 생성
		Thread t = new Thread(new MyThread());
		t.start();
	}
}
// 결과

thread 실행
  • Runnable의 구현체일 경우 Thread 생성자에 MyThread 객체를 인자로 넣어서 쓰레드를 인스턴스화 시키면 된다. 
  • 보통 Thread를 사용할 때 Thread 클래스보다는 Runnable 인터페이스를 더 많이 사용한다. 

쓰레드의 상태 

https://levelup.gitconnected.com/states-of-thread-in-java-6f7bc1c628b9
Thread life cycle

1. New 

  • Thread가 새롭게 생성된 상태
Thread newThread = new newThreadClass();

2. Runnable(Running or Ready state)

  • Thread가 실행 가능한(ready) 상태거나 실행 중인(Running) 상태
Thread newThread = new newThreadClass();
newThread.start();

3. Blocked

  • Thread가 실행 중지 상태이며, 모니터 락(monitor lock)이 풀리기를 기다리는 상태
  • Blocked 는 공유객체의 synchronized 메소드 synchronized 블록을 수행하는 스레드가 있는 상황에서 이 공유 객체에 접근하는 다른 스레드들이 얻게되는 상태변화이다. 
  • Blocked 상태에 있는 Thread는 CPU 사용 권한을 포기하고 실행을 일시적으로 중지한다. 

4. Waiting / Timed-Waiting

  • 실행 가능하지 않은 일시정지 상태 
  • Timed-Waitingsleep() 메소드를 사용하면 이뤄지는 상태변화이다. 
  • Waitingwait() 메소드나 join() 메소드를 사용하면 이뤄지는 상태변화이다. 

5. Terminate 

  • Thread의 작업이 종료된 상태 

쓰레드의 스케쥴링과 관련된 메서드

▶ sleep(long millis) - 일정시간동안 쓰레드를 멈추게한다.

    static void sleep(long millis)
    static void sleep(long millis, int nanos)
  • sleep() 에 의해 일시정지 상태가 된 쓰레드는 지정된 시간이 다 되거나, interrupt()가 호출되면 (InterruptedException 발생), 실행대기 상태(waiting)가 된다. 
  • 따라서, sleep() 을 호출할 때는 항상 try-catch 문으로 예외를 처리해 줘야한다. 

▶ interrupt() 와 interripted() - 쓰레드의 작업을 취소한다. 

  • 진행 중인 쓰레드의 작업이 끝나기 전에 취소시켜야 하는 경우 interrupt()를 사용하여 쓰레드에게 작업을 멈추라고 요청할 수 있다. 
  • 단지 멈추라고 요청만 할 뿐 쓰레드를 강제로 종료시키지는 못한다. 
  • interrupted()는 쓰레드에 대해 interrupt() 가 호출되었는지 알려준다. interrupt()가 호출되지 않았다면 false를, 호출되었다면 true를 반환한다.
    Thread t = new Thread();
    th.start();
      ...
    th.interrupt();
    // void interrupt() : 쓰레드의 interrupted 상태를 false에서 true로 변경

    class MyThread extends Thread {
        public void run() {
            while(!interrupted()) {
                ...
            }
        }
     }

▶ suspend(), resume(), stop()

  • suspend()는 sleep()처럼 쓰레드를 멈추게 한다. suspend()에 의해 정지된 쓰레드는 resume()을 호출해야 다시 실행 대기 상태가 된다. 
  • stop()은 호출되는 즉시 쓰레드가 종료된다. 
  • suspend(), resume(), stop()은 쓰레드의 실행을 제어하는 가장 손쉬운 방법이지만, suspend()와 stop()이 교착상태(deadlock)을 일으키기 쉽게 작성되어 있으므로 사용이 권장되지 않는다. 
  • 따라서, 이 메서드들은 deprecated(사용 안할 것을 (강력히) 권장) 되었다. 
// JAVA API 문서 stop() 

void stop(Throwable obj)  
Deprecated.
This method was originally designed to force a thread to stop and throw a given Throwable
as an exception. It was inherently unsafe (see stop() for details), and furthermore could be 
used to generate exceptions that the target thread was not prepared to handle.

▶ yield() - 다른 쓰레드에게 양보한다.

  • yield()는 쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보한다. 
  • 예를들어, 스케줄러에 의해 1초의 실행시간을 할당받은 쓰레드가 0.5초의 시간동안 작업한 상태에서 yield()가 호출되면, 나머지 0.5초는 포기하고 다시 실행대기상태가 된다.

▶ join() - 다른 쓰레드의 작업을 기다린다.

  • join()은 자신의 작업 중간에 다른 쓰레드의 작업을 참여(join) 시킨다는 의미로 이름 지어진 것이다.
  • 쓰레드 자신이 하던 작업을 잠시 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도록 할 때 사용한다.
  • 시간을 지정하지 않으면, 해당 쓰레드가 작업을 모두 마칠 때 까지 기다리게 된다. 
  • 작업 중에 다른 쓰레드의 작업이 먼저 수행되어야 할 필요가 있을 때 join()을 사용한다. 
    void join()
    void join(long millis)
    void join(long millis, int nanos)
    try {
        th1.join()
    } catch (InterruptedException e) { }
  • join()도 sleep()처럼 interrupt()에 의해 대기 상태에서 벗어날 수 있으며, join()이 호출되는 부분을 try-catch 문으로 감싸야한다. 

쓰레드의 우선순위

  • Java에서 각 쓰레드는 우선순위(Priority)에 관한 자신만의 필드를 가지고 있다. 
  • 이러한 우선순위에 따라 특정 쓰레드가 더 많은 시간 동안 작업할 수 있도록 설정한다. 
  • 쓰레드가 가질 수 있는 우선순위의 범위는 1~10이며 숫자가 높을 수록 우선순위가 높다. 
  • main() 메소드를 실행하는 쓰레드의 우선순위는 언제나 5이다.
필드 설명
static int MAX_PRIORITY 쓰레드가 가질 수 있는 최대 우선순위를 명시한다.
static int MIN_PRIORITY 쓰레드가 가질 수 있는 최소 우선순위를 명시한다. 
static int NORM_PRIORITY 쓰레드가 생성될 때 가지는 기본 우선순위를 명시한다. 
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    // 보통 우선순위

Main 쓰레드

  • main 메서드의 작업을 수행하는 것도 쓰레드이며, 이를 main 쓰레드라고 한다. 
  • 프로그램을 실행하면 기본적으로 하나의 쓰레드를 생성하고, 그 쓰레드가 main 메서드를 호출해서 작업이 수행되는 것이다. 
  • 보통 main 메서드가 수행을 마치면 프로그램이 종료되지만, main 메서드는 수행을 마쳤는데 다른 쓰레드가 아직 작업을 마치지 않은 상태라면 프로그램이 종료되지 않는다. 

https://www.geeksforgeeks.org/main-thread-java/?ref=lbp

public class ThreadEx implements Runnable{
	@Override
	public void run() {}
	
	public static void main(String[] args) {
		Thread t1 = Thread.currentThread(); // 현재 실행 중인 쓰레드의 참조를 반환
		System.out.println("CurrentThread = "+t1);
		
		Thread t2 = new Thread(new ThreadEx());
		System.out.println("newThread = " +t2);
	}
}
// 결과

CurrentThread = Thread[main,5,main]
newThread = Thread[Thread-0,5,main]
  • 메인 메서드에서 현재 쓰레드를 참조하면 main 쓰레드가 반환되는 것을 확인할 수 있다. 

동기화

  • 멀티쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유하기 때문에 서로의 작업에 영향을 줄 수 있다. 
  • 이러한 일을 방지하기 위해서 한 쓰레드가 특정 작업을 끝마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 하는 것이 필요하다. 그래서 도입된 개념이 '임계 영역(critical section)''잠금(락, lock)' 이다. 
  • 공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정해 놓고, 공유 데이터(객체)가 가지고 있는 lock을 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 한다. 
  • 그리고 해당 쓰레드가 임계 영역 내의 모든 코드를 수행하고 벗어나서 lock을 반납해야만 다른 쓰레드가 반납된 lock을 획득하여 임계 영역의 코드를 수행할 수 있게 한다.
  • 이렇게 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것'쓰레드의 동기화(synchronization)'라고 한다. 

synchronized 키워드

// 메서드 전체를 임계영역으로 설정
public synchronized void method1 () {
    ......
}

// 특정한 영역을 임계영역으로 설정
synchronized(객체의 참조변수) {
    ......
}
  • 메서드의 반환 타입앞에 synchronized 키워드를 붙여서 메서드 전체를 임계 영역으로 설정할 수 있다. 쓰레드는 synchronized 키워드가 붙은 메서드가 호출된 시점부터 해당 메서드가 포함된 객체의 lock(자물쇠)을 얻어 작업을 수행하다가 메서드가 종료되면 lock 을 반환한다. 
  • 메서드 내의 코드 일부를 블록으로 감싸고 블록 앞에 synchronized(참조 변수)를 붙이는 방법이 있다. 이때 참조 변수는 락을 걸고자 하는 객체를 참조하는 것이어야 한다. 이 영역으로 들어가면서 쓰레드는 지정된 객체의 lock을 얻게 되고 블록을 벗어나면 lock을 반납한다. 
  • synchronized를 이용한 동기화는 지정된 영역의 코드를 한 번에 하나의 쓰레드가 수행하는 것을 보장한다. 
class Account {
    private int balance = 1000; //잔고

    public int getBalance() {
        return balance;
    }

    public void withdraw(int money) {
        // 잔고가 출금액보다 클때만 출금을 실시하므로 잔고가 음수가 되는 일은 없어야함
        if (balance >= money) {
            try {
                // 문제 상황을 만들기 위해 고의로 쓰레드를 일시정지
                Thread.sleep(1000);
            } catch (InterruptedException e) {}

            balance -= money;
        }
    }
}


public class ThreadEx implements Runnable{
	Account account = new Account();
	@Override
	public void run() {
	    while (account.getBalance() > 0) {
	        // 100, 200, 300 중 임의의 값을 선택해서 출금
	        int money = (int) (Math.random() * 3 + 1) * 100;
	        account.withdraw(money);
	        System.out.println("balance: " + account.getBalance());
	    }
	}
	public static void main(String[] args) {
        Runnable r = new ThreadEx();
        new Thread(r).start();
        new Thread(r).start();
	}
}
// 결과

balance: 900
balance: 600
balance: 300
balance: 0
balance: -200
  • 쓰레드 하나가 if문을 통과하면서 balance를 검사하고 순서를 넘기는 사이에 다른 쓰레드가 출금을 실시해서 실제 balance가 if문을 통과할 때 검사했던 balance보다 적어지게 된다. 하지만 이미 if 문을 통과했기 때문에 출금은 이루어지게 된고 음수가 나오게 된다. 
  • 이를 해결하기 위해 출금하는 로직에 동기화를 해서 한 쓰레드가 출금 로직을 실행하는 동안 다른 쓰레드가 출금 블록에 들어오지 못하도록 막아줘야 한다.
class Account {
    private int balance = 1000; //잔고

    public int getBalance() {
        return balance;
    }

    public void withdraw(int money) {
        // synchronized 키워드를 이용하여 임계영역 설정
        synchronized(this) {
    	if (balance >= money) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}

            balance -= money;
        }
       }
    }
}


public class ThreadEx implements Runnable{
	Account account = new Account();
	@Override
	public void run() {
	    while (account.getBalance() > 0) {
	        // 100, 200, 300 중 임의의 값을 선택해서 출금
	        int money = (int) (Math.random() * 3 + 1) * 100;
	        account.withdraw(money);
	        System.out.println("balance: " + account.getBalance());
	    }
	}
	public static void main(String[] args) {
        Runnable r = new ThreadEx();
        new Thread(r).start();
        new Thread(r).start();
	}
}
// 결과

balance: 800
balance: 700
balance: 600
balance: 500
balance: 400
balance: 100
balance: 0
balance: 0

DeadLock(교착상태)

p1과 p2가 리소스1,2 둘 다를 얻어야 한다고 가정할 때,

t1에 p1이 리소스 1을 얻고, p2가 리소스 2를 얻었다면, t2때 p1은 리소스 2를, p2는 리소스 1을 기다리게 된다.

하지만 서로 원하는 리소스가 상대방에게 할당되어 있기 때문에 이 두 프로세스는 무한정 기다리게 되는데 이러한 상태를 DeadLock 상태라고 한다.

 

교착상태(데드락, deadlock)은 두 개 이상의 작업이 서로 상대방의 작업이 끝나기를 기다리고 있어서 아무것도 완료되지 못하는 상태를 말한다. 데드락은 한 시스템 내에서 다음의 네 가지 조건이 동시에 성립할 때 발생한다. 아래 네가지 조건 중 하나라도 성립하지 않도록 만든다면 교착 상태를 해결할 수 있다. 

교착상태의 조건 

상호배제(Mutual exclusion): 자원은 한 번에 한 프로세스 만이 사용할 수 있어야 한다. 

점유대기(Hold and wait): 최소한 하나의 자원을 점유하고 있으면서 다른 프로세스에 할당되어 사용되고 있는 자원을 추가로 점유하기 위해 대기하는 프로세스가 있어야 한다. 즉, 이미 자원을 사용 중인데, 다른 프로세스가 사용중인 자원을 사용하기 위해 대기하고 있는 상태의 프로세스가 존재해야 한다.

비선점(No Preemption): 다른 프로세스에 할당된 자원은 사용이 끝날 때까지 강제로 빼앗을 수 없어야 한다.

순환대기(Circular wait): 프로세스의 집합에서 p0은 p1이 점유한 자원을 대기하고 p1은 p2가 점유한 자원을 대기하고, p2..pn-1은 pn이 점유한 자원을 대기하며 pn은 p0이 점유한 자원을 요구해야한다. 

 

 

 

 

댓글