- 프로세스와 쓰레드
- 쓰레드의 구현과 실행
- start()와 run()
- 싱글쓰레드와 멀티쓰레드
- 쓰레드의 우선순위
- 쓰레드 그룹
- 데몬 쓰레드
- 쓰레드의 실행제어
프로세스(process)란 간단히 말해서 실행 중인 프로그램
이다. 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 된다.
프로세스는 프로그램을 수행하는데 필요한 데이터와 메모리등의 자원 그리고 쓰레드로 구성되어 있으며 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 바로 쓰레드이다. 모든 프로세스에는 최소한 하나 이상의 쓰레드가 존재하며, 둘 이상의 쓰레드를 가진 프로세스를 멀티쓰레드 프로세스
라고 한다.
하나의 프로세스가 가질 수 있는 쓰레드의 개수는 제한되어 있지 않으나 쓰레드가 작업을 수행하는데 개별적인 메로리 공간을 필요로 하기 때문에 프로세스의 메모리 한계에 따라 생성할 수 있는 쓰레드의 수가 결정된다.
대부분의 OS는 멀티태스킹을 지원하기 때문에 여러개의 프로세스가 동시에 실행될 수 있다.
멀티쓰레딩은 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것이다. CPU의 코어가 한번에 단 하나의 작업만을 수행할 수 있으므로, 실제로 동시에 처리되는 작업의 개수는 코어의 개수와 일치한다. 처리해야하는 쓰레드의 수는 언제나 코어의 개수보다 훨씬 많기 때문에 각 코어가 아주 짧은 시간동안 여러 작업을 번갈아 가며 수행함으로써 여러 작업들이 모두 동시에 수행되는 것처럼 보이게 한다.
장점 : CPU의 사용률을 향상시킨다. 자원을 보다 효율적으로 사용할 수 있다. 사용자에 대한 응답성이 향상된다. 작업이 분리되어 코드가 간결해진다.
멀티쓰레드에는 장점만 있는 것은 아니어서 여러 쓰레드가 프로세스 내에서 자원을 공유하면서 작업을 하기 때문에 발생할 수 있는 동기화(synchronizaion), 교착상태(deadlock)와 같은 문제들을 고려해서 신중히 프로그래밍해야 한다.
쓰레드를 구현하는 방법은 Thread클래스를 상속받는 방법과 Runnable인터페이스를 구현하는 방법이 있다. Rnnable인터페이스를 구현하는 방법이 일반적이다. ( Thread클래스 상속시 다른 클래스 상속불가 ) Runnable인터페이스를 구현하는 방법은 재사용성이 높고 코드의 일관성을 유지할 수 있기 때문에 보다 객체지향적인 방법이라 할 수 있다.
1. Thread클래스를 상속
class MyThread extend Thread{
public void run(){ /*작업내용*/ } // Thread클래스의 run()오버라이딩
}
2. Runnable인터페이스 구현
class MyThread implments Runnable{
public void run(){ /*작업내용*/} // Runnable인터페이스의 run() 구현
}
Runnable인터페이스는 오직 Run()만 정의되어 있는 간단한 인터페이스이다. 해당 인터페이스 구현을 위해 run()의 몸통{}을 만들어 주면 된다.
즉, 쓰레드를 구현한다는 것은 그저 쓰레드를 통해 작업하고자 하는 내용으로 run()의 몸통을 채우는 것일 뿐이다.
Thread클래스를 상속받은 경우와 Runnable인터페이스를 구현한 경우의 인스턴스 생성 방법이 다르다.
ThreadEx1_1 t1 = new ThreadEx1_1(); // Thread의 자손클래스의 인스턴스를 생성
Runnable r = new ThreadEx1_2(); // Runnable을 구현한 클래스의 인스턴스를 생성
Thread t2 = new Thread(r); // 생성자 Thread(Runnable target)
Thread t2 = new Thread(new ThreadEx1_2()); //위의 두줄 간단히
Runnable인터페이스를 구현한 경우, Runnable인터페이스를 구현한 클래스의 인스턴스를 생성한 다음, 이 인스턴스를 Thread클래스의 생성자의 매개변수로 제공해야 한다.
Thraed클래스를 상속받으면, 자손 클래스에서 조상인 Thread의 클래스의 메서드를 직접 호출 할 수 있지만, Runnable을 구현하면 Thread클래스의 static메서드인 currentThread()를 호출하여 쓰레드에 대한 정보를 얻어와야만 호출이 가능하다.
static Thread currentThread() 현재 실행중인 쓰레드의 참조를 반환
String getName() 쓰레드의 이름을 반환
쓰레드의 이름은 생성자나 메서드를 통해서 지정또는 변경 할 수 있다.
Thread(Runnable target,String naem) Thread(String name) void setName(String name)
이름을 지정하지 않으면
Thread-번호
형식으로 이름이 정해진다.
쓰레드를 생성했다고 해서 자동으로 실행되는 것은 아니다. start()를 호출해야만 쓰레드가 실행된다.
t1.start();
t2.start();
한번 실행이 종료된 쓰레드는 다시 실행할 수 없다. 즉, 하나의 쓰레드에 대해 start()가 한번만 호출될수 있다는 뜻이다. 그래서 쓰레드의 작업을 한번더 수행해야 한다면 새로운 쓰레드를 생성한 다음 start()를 호출해야 한다.
ThreadEx1_1 t1 = new ThreadEx1_1();
t1.start();
t1 = new ThreadEx1_1(); //다시 생성
t1.start();
main메서드에서 run()을 호출한다는 것은 생성된 쓰레드를 실행시키는 것이 아닌 단순히 클래스에 선언된 메서드를 호출하는 것일 뿐이다.
반면에 start() 는 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택을 생성한 다음에 run()을 호출해서, 생성된 호출스택에 run()이 첫번째로 올라가게 한다.
모든 쓰레드는 독립적인 작업을 수행하기 위해 자신만의 호출스택을 필요로 하기 때문에, 새로운 쓰레드를 생성하고 실행시킬 때마다 새로운 호출스택이 생성되고 쓰레드가 종료되면 작업에 사용된 호출스택은 소멸된다.
호출스택에서는 가장 위에 있는 메서드가 현재 실행중인 메서드이고 나머지 메서드들은 대기상태에 있다. 쓰레드가 둘 이상일때는 호출스택의 최상위에 있는 메서드일지라도 대기상태에 있을 수 있다. 스케줄러는 실행대기중인 쓰레드들의 우선순위를 고려하여 실행순서와 실행시간을 결정하고, 각 쓰레드들은 작성된 스케줄에 따라 자신의 순서가 되면 지정된 시간동안 작업을 수행한다.
주어진 시간동안 작업을 마치지 못한 쓰레드는 다시 자신의 차례가 돌아올 때까지 대기상태로 있게 되며, 작업을 마친 쓰레드(run()의 수행이 종료)는 호출스택이 비워지며 이 쓰레드가 사용하던 호출스택은 사라진다.
main메서드의 작업을 수행하는 것도 쓰레드이며, 이를 main쓰레드라고 한다. 작업을 수행하면 일꾼이 최소한 하나는 필요하다ㅏ. 그래서 프로그램을 실행하면 기본적으로 하나의 쓰레드(일꾼)을 생성하고, 그 쓰레드가 main메서드를 호출해서 작업이 수행되도록 하는 것이다.
main메서드가 수행을 마쳤다하더라도 다른 쓰레드가 아직 작업을 마치지 않은 상태라면 프로그램이 종료되지 않는다.
실행중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다.
하나의 쓰레드로 두개의 작업을 수행한 시간과 두개의 쓰레드로 두개의 작업을 수행한 시간은 거의 같다. 오히려 두개의 쓰레드로 작업한 시간이 싱글 쓰레드로 작업한 시간보다 더 걸리게 되는데 그 이유는 쓰레드간의 작업전환 시간이 걸리기 때문이다. 그래서 싱글 코어에서 단순히 CPU만을 사용하는 계산작업이라면 오히려 멀티쓰레드보다 싱글쓰레드로 프로그래밍하는 것이 더 효율적이다.
멀티코어에서는 멀티쓰레드로 두 작업을 수행하면, 동시에 두 쓰레드가 수행될 수 있으므로 두 작업이 겹치는 부분이 발생한다.
두 쓰레드가 서로 다른 자원을 사용하는 작업의 경우에는 싱글쓰레드 프로세스보다 멀티쓰레드 프로세스가 더 효율적이다. 예로, 사용자로부터 데이터를 입력받는 작업, 네트워크로 파일을 주고받는 작업, 프린터로 파일을 출력하는 작업과 같이 외부기기와의 입출력을 필요로 한 경우가 이에 해당한다.
쓰레드는 우선순위라는 속성을 가지고 있는데 , 이 우선순위의 값에 따라 쓰레드가 얻는 실행시간이 달라진다. 쓰레드가 수행하는 작업의 중요도에 따라 쓰레드의 우선순위를 서로 다르게 지정하여 특정 쓰레드가 더 많은 작업시간을 갖도록 할 수 있다. (시각적인 부분이나 사용자에게 빠르게 반응해야하는 작업을 하는 쓰레드의 우선순위는 달느 작업을 수행하는 쓰레드에 비해 높아야 한다. )
void setPriority(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~10이며 높을수록 우선순위가 높다. 쓰레드의 우선순위는 쓰레드를 생성한 쓰레드로부터 상속받는다. (main메서드를 수행하는 쓰레드는 우선순위가 5이므로 main메서드내에서 생성하는 쓰레드의 우선순위는 자동적으로 5가 된다.)
쓰레드 그룹은 서로 관련된 쓰레드를 그룹으로 다루기 위한 것으로, 폴더를 생성해서 관련된 파일들을 함께 넣어서 관리하는 것처럼 쓰레드 그룹을 생성해서 쓰레드를 그룹으로 묶어서 관리할 수 있다. 쓰레드 그룹에 다른 쓰레드 그룹을 포함시킬 수 있다.
쓰레드를 쓰레드 그룹에 포함시키려면 Thread생성자를 이용해야 한다.
Thread(ThreadGroup group,String name)
Thread(ThreadGroup group,Runnable target)
Thread(ThreadGroup group,Runnable target,String name)
Thread(ThreadGroup group,Runnable target,String name,long stackSize)
모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 하기 때문에, 위와 같이 쓰레드 그룹을 지정하는 생성자를 사용하지 않은 쓰레드는 기본적으로 자시을 생성한 쓰레드와 같은 쓰레드 그룹에 속하게 된다.
데몬쓰레드는 다른 일반 쓰레드의 작업을 돕는 보조적인 역할을 수행하는 쓰레드이다. 일반 쓰레드가 모두 종료되면 데몬 쓰레드는 강제적으로 자동종료되는데, 그 이유는 데몬 쓰레드는 일반 쓰레드의 보조역할을 수행하므로 일반 쓰레드가 모두 종료되고 나면 데몬 쓰레드의 존재의 의미가 없기 때문이다. 이점을 제외하고 데몬 쓰레드는 일반 쓰레드와 크게 다르지 않ㄴ다. 예로는 가비지 컬렉터, 워드프로세서의 자동저장, 화면 자동갱신 등이 있다.
sleep()은 지정된 시간동안 쓰레드를 멈추게 한다.
static void sleep(long milis)
static void sleep(long millis,int nanos)
밀리세컨드와 나노세컨드의 시간단위로 세밀하게 값을 지정할 수 있지만 어느 정도의 오차가 발생할 수 있다는 점을 염두에 둬야 한다.
sleep()에 의해 일시정지 상태가 된 쓰레드는 지정된 시간이 다 되거나 interrupt()가 호출되면 잠에서 깨어나 실행대기 상태가 된다. 그래서 sleep()을 호출할 때는 try-catch문으로 예외를 처리해줘야 한다. ( try-catch문까지 포함하는 새로운 메서드를 만들어서 사용하기도 한다. )
sleep()은 항상 현재 실행중인 쓰레드에 대해 작동하기 때문에 실제로 영향을 받는 것은 main메서드를 실행하는 메인 쓰레드이다. 그래서 참조변수를 이용해서 호출하기 보다는 Thread.sleep(2000)
과 같이 해야 한다.
진행 중인 쓰레드의 작업이 끝나기 전에 취소시켜야 할 때가 있다. interrupt() 는 쓰레드에게 작업을 멈추라고 요청한다. 멈추라고 요청만 하는 것일 뿐 쓰레드를 강제로 종료시키지는 못한다. 그저 쓰레드의 interrupted상태를 바꾸는 것일 뿐이다.
interrupted() 는 쓰레드에 대해 interrupt()가 호출되었는지를 알려준다.
void interrupt() : 쓰레드의 interrupted상태를 false에서 true로 변경
boolean isInterrupted() : 쓰레드의 interrupted상태를 반환
static boolean interrupted() : 현재 쓰레드의 interrupted상태를 반환후, false로 변경
suspend()는 sleep()처럼 쓰레드를 멈추게 한다. suspend()에 의해 정지된 쓰레드는 resume()을 호출해야 다시 실행대기 상태가 된다. stop()은 호출되는 즉시 쓰레드가 종료된다. 사용을 권장하지 않는다.(교착상태)
yield()는 쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보한다.
쓰레드는 자신이 하던 작업을 잠시 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도록 할 때 join()을 사용한다. 시간을 지정하지 않으면 해당 쓰레드가 모든 작업을 마칠 때까지 기다리게 된다. 작업 중에 다른 쓰레드의 작업이 먼저 수행되어야 할 필요가 있을 때 join()을 사용한다.
싱글쓰레드 프로세스의 경우 프로세스 내에서 단 하나의 쓰레드만 작업하기 때문에 프로세스의 자원을 가지고 작업하는데 별문제가 없지만, 멀티쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업하기 때문에 서로의 작업에 영향을 주게된다.
이러한 일이 발생하는 것을 막기 위해 한 쓰레드가 특정 작업을 끝마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 하는 것이 필요하다. 그래서 도입된 개념이 임계 영역(critical section)
과 잠금(lcok)
이다.
공유데이터를 사용하는 코드영역을 임계 영역으로 지정해놓고, 공유 데어티가 가지고 있는 락을 휙득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 한다. 락을 반납해야만 다른 쓰레드가 반납한 락을 휙득하여 임계 영역의 코드를 수행할 수 있게 된다.
한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 쓰레드의 동기화
라고 한다.
이 키워드는 임계영역을 설정하는데 사용된다.
1. 메서드 전체를 임계 영역으로 지정
public synchronized void calcSum(){ ... }
2. 특정한 영역을 임계 영역으로 지정
synchronized(객체의 참조변수) { ... }
첫번째 방법은 메서드 앞에 synchronized를 붙이는 것으로 메서드 전체가 임계영역으로 설정된다. 호출된 시점부터 해당 메서드가 포함된 객체의 lock을 얻어 작업을 수행하다가 메서드가 종료되면 lock을 반환한다. 두번째 방법은 메서드 내의 코드 일부를 블럭{}으로 감싸고 블럭 앞에 synchronized
를 붙이는 것인데 , 이때 참조변수는 락을 걸고자 하는 객체를 참조하는 것이어야 한다. 이 블럭의 영역 안으로 들어가면서 부터 쓰레드는 지정된 객체의 락을 얻게 되고, 이 블ㄹ겅르 벗어나면 락을 반납한다.
동기화해서 공유 데이터를 보호하는 것 까지는 좋은데, 특정 쓰레드가 객체의 락을 가진 상태로 오랜시간을 보내지 않도록 하는 것도 중요하다. 이러한 상황을 개선하기 위해 고안된것이 wait()와 notify()이다.
동기화된 임계영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면, 일단 wait()을 호출하여 쓰레드가 락을 반납하고 기다리게 한다. 그러면 다른 쓰레드가 락을 얻어 해당 객체에 대한 작업을 수행할 수 있게 된다. 나중에 작업을 진행할 수 있는 상황이 되면 notify()를 호추랳서, 작업을 중단했던 쓰레드가 다시 락을 얻어 작업을 진행할 수 있게 한다.