우리가 작성한 자바 프로그램이 실행이 되어서 RAM에 올라가면 프로세스가 된다.
즉, 우리가 실행하고 있는 크롬 브라우저, 디스코드, 인텔리제이 등이 하나의 프로세스가 된다.
프로세스를 구성하는 요소 중 하나이다. 쓰레드는 프로세스를 구성하는 또 다른 요소인 데이터, 메모리 등의 자원을 활영해서 우리가 명령하는 작업들을 수행해준다.
디스코드의 음성 대화 등등의 동작들을 쓰레드가 수행해준다고 생각하면 된다.
모든 프로세스는 최소한 하나 이상의 프로세스로 이루어져 있으며 여러개의 쓰레드를 활용하는 프로세스를 멀티 쓰레드 프로세스라고 부른다.
두 가지 방법으로 쓰레드를 구현할 수 있다.
Thread class
상속받기Runnalbe interface
구현하기상속을 통해서 구현하면 다른 클래스를 상속받지 못하기 때문에 Runnalbe
을 구현하는 방식을 많이 사용한다.
반면, Runnable
은 run()
메소드만 구현할 수 있기 때문에 더 많은 메소드를 오버라이딩하기 위해서는 Thread
를 상속받아야 한다.
쓰레드를 구현하기 위해서 Thread
를 상속받아야 한다.
class MyExtendThread extends Thread {
@Override
public void run() {
for (int i=0; i<5; i++)
System.out.prinlnt(getName());
}
}
Thread
의 run
메소드를 오버라이딩 해야한다.
Runnable
을 구현하여 Thread
를 사용하자
class MyRunnalbeThread implements Runnalbe {
@Override
public void run() {
for (int i=0; i<5; i++)
System.out.println(Thread.currentThread().getName());
}
}
마찬가지로 run()
을 오버라이딩하는데 Thread
의 정보를 알 수 없으니 static method
인 Thread.currentThread()
를 호출해서 실행중인 스레드를 리턴받을 수 있다.
public static void main(String args[]) {
MyExtendThread t1 = new MyExtendThread();
Runnable r = new MyRunnalbeThread();
Thread t2 = new Thread(r);
// start() 메소드는 run() 메소드를 호출해준다.
t1.start();
t2.start();
}
Runnable
을 구현한 경우에는 Runnable
타입의 MyRunnableThread
인스턴스를 생성한 후 Thread
의 생성자에 매개변수로 넘겨준다.
start()
메소드는 한번만 호출될 수 있다.
다시 실행하고 싶다면 새로운 인스턴스를 생성하여 start()
를 호출해야 한다.
간단하게 이야기하면 두 메소드의 차이점은 다음과 같다.
start()
: 쓰레드를 실행run()
: 클래스의 멤버 메소드를 호출조금 더 자세히 이야기 하자면 JVM의 Call Stack에서 차이가 난다.
Call Stack이 여러개가 되면 스케쥴러의 알고리즘에 따라서 번갈아가며 작업이 수행된다.
Call Stack에 쌓인 모든 작업을 마무리하면 Call Stack은 사라진다.
그림에서 알 수 있지만 새로운 Call Stack에는 run()
메소드만 존재한다.
따라서 위에서 작성한 MyExtendThread
의 코드를 다음과 같이 수정해서 Call Stack에 run()
만 존재하는지 직접 확인해보자
class MyExtendThread extends Thread {
@Override
public void run() {
try {
throw new Exception();
} catch (Exception e) {
e.printStackTrace();
}
}
}
그러면 아래와 같이 run()
메소드만 존재한다는 것을 확인 할 수 있다.
일단 표를 참고하자
STATE | Description |
---|---|
NEW | 쓰레드가 생성은 되었지만 start()를 호출하지 않은 상태 |
RUNNALBE | 실행 중 혹은 실행 가능 |
BLOCKED | 동기화 블록에 의해서 일시정지된 상태 (lock이 풀릴 때 까지 기다리는 상태) |
WAITING | 쓰레드의 작업이 종료되지는 않았지만 쓰레드가 실행 가능하지 않은 일시정지 상태 |
TIMED_WAITING | WAITING 상태 + 일시정지시간이 지정된 상태 |
TERMINATED | 쓰레드의 작업이 종료된 상태 |
쓰레드의 상태는 Thread
의 여러가지 메소드와 스케쥴러에 의해서 변경될 수 있다. 그림으로 표현하면 이해가 쉬울 것 같아서 그림을 첨부한다.
Thread
는 priority
라는 멤버변수를 가지고 있다. (private int priority
)
이 우선순위의 값에 따라 쓰레드가 스케쥴러에게 할당받는 실행시간이 달라진다. 우선순위를 조작하여 중요한 쓰레드에게 더 많은 실행시간을 할당할 수 있다.
쓰레드가 가질 수 있는 우선순위 값의 범위는 1~10이며 숫자가 높을수록 우선순위가 높다.
main()
메소드의 우선순위는 5
로 설정된다.
우선순위는 getPriority()
를 호출해서 확인할 수 있고 setPriority()
를 호출해서 설정할 수 있다.
psvm
을 실행시켜주는 쓰레드이다.
우선순위는 5로 설정된다.
쓰레드들은 소속된 프로세스의 자원들을 공유하기 때문에 동기화가 반드시 필요하다.
동기화가 없다면 쓰레드의 작업 순서를 예측할 수 없기 때문에 변수가 원치 않은 값으로 변경될 수 있다.
synchronized 영역은 임계구역(critical section) 이라고도 하며 짧을 수록 좋다.
문법은 아래와 같다.
synchronized (expression) {
statements
}
expression
에는 배열 혹은 객체를 넣어야한다.
statements
에는 손상이 우려되는 코드를 넣어야한다.
statement block
을 실행하기 전에 자바 인터프리터는 expression
에 작성한 객체나 배열에 lock을 건다.
이 lock은 statement block
이 끝날 때 까지 지속된다. 물론 끝나면 다시 해제한다.(release)
expression
에 lock이 걸려있을 때는 다른 스레드에서 lock을 걸 수 없다.
먼저 lock을 걸고 작업 중인 쓰레드가 있다면 작업이 모두 끝날 때 까지 다른 쓰레드에서 데이터를 변경할 수 없게 만든다 대부분 뒤 늦게 접근한 쓰레드는 일시정지 상태에 빠진다.
syncronized
는 메소드에도 선언할 수 있다. 메소드에 선언되면 당연히 메소드의 모든 동작이 syncronized
에 의해 다뤄진다는 것을 나타낸다.
synchronized instance method
를 실행할 때 자바는 class instance
에 lock을 건다. 이는 아래와 같다고 볼 수 있다
public synchronized void sync() {
// synchronized가 선언된 메소드의 모든 동작
}
synchronized method
를 이미 다른 쓰레드가 호출해서 작업중이라면 다른 쓰레드는 사용할 수 없다.
public class MyRunnableThread implements Runnalbe {
int instance = 0; // Thread 간 공유됨
@Override
public void run() {
int local = 0; // Thread 간 공유되지 않음
String name = Thread.currentThread().getName();
while (i < 3) {
System.out.println(name + " Local i:" + ++local);
System.out.println(name + " Instance i:" + ++instance);
System.out.println();
}
}
}
위 코드의 결과는 아래와 같다
공유되는 instance
는 총 6번 실행되어 6의 값을 가진다.
class MyExtend Thread extends Thread {
Data d; // 공유됨
public MyExtendThread(Data d) {
this.d = d;
}
@Override
public void run() {
int local = 0; // 공유되지 않음
while (local < 3) {
System.out.println(getName() + " Local i: " + ++local);
System.out.println(getName() + " Instance i: " + ++d.instance);
System.out.println();
}
}
}
...
...
public static void main(String[] args) {
Data d = new Data();
MyExtendThread t1 = new MyExtendTread(d);
MyExtendThread t2 = new MyExtendTread(d);
t1.start();
t2.start();
}
마찬가지로 결과는 아래와 같다.
반면에 Thread
를 상속 받는 경우에 아래와 같은 코드는 인스턴스 변수더라도 쓰레드 간 공유를 하지 않는다.
class MyExtend Thread extends Thread {
int instance = 0; // 공유되지 않음
@Override
public void run() {
int local = 0; // 공유되지 않음
while (local < 3) {
System.out.println(getName() + " Local i: " + ++local);
System.out.println(getName() + " Instance i: " + ++d.instance);
System.out.println();
}
}
}
...
...
public static void main(String[] args) {
MyExtendThread t1 = new MyExtendTread();
MyExtendThread t2 = new MyExtendTread();
t1.start();
t2.start();
}
지역 변수는 각 쓰레드의 스택 내에 생성되므로 쓰레드간 공유가 되지 않는다!
모든 인스턴스 메소드에는 참조변수 this
와 super
가 숨겨져 있기 때문에 인스턴스 변수에 접근할 수 있다!
동기화는 위에서 언급한 것 처럼 synchronized
를 활용해서 쉽게 구현할 수 있다.
만약 다른 쓰레드가 이미 작업중인 synchronized
영역에 접근을 시도한다면 대기 상태에 빠지게 되고 우리가 작성한 코드에 논리적인 오류가 있다면 모든 쓰레드가 대기 상태에 빠지는 데드락 상태에 빠질 수 있다.
stop()
, suspend()
, resume()
메소드는 데드락을 일으킬 가능성이 높기 때문에 deprecated 되었다. 없다고 생각하자
데드락에 빠지지 않도록 wait()
, notify()
,제어문, 변수를 활용해서 쓰레드를 제어하자
wait()
,notify()
,notifuAll()
특징
Object
클래스에 정의가 되어 있다.synchronized
블록안에서만 사용이 가능하다
wait()
wait()
, wait(long)
, wait(long, int)
의 형태로 오버로딩이 되어 있다.
또한 InterruptedException
을 throws
하기 때문에 예외처리가 반드시 필요하다.
wait()
메소드가 호출되면 해당 스레드는 waiting pool
에서 자신이 걸어놓은 lock을 모두 풀고 대기한다.
waiting pool
은 객체마다 공유되지 않는다.
매개변수가 있는 wait()
메소드를 호출하면 notify
메소드로 깨우지 않아도 매개변수에 맞는 시간이 흐른 후 자동으로 쓰레드가 실행된다.
notify()
, notifyAll()
wait()
중인 쓰레드를 다른 쓰레드에서 notify()
를 통해 깨워줘야 한다.
또, notifyAll()
을 사용하면 waiting pool
에 있는 모든 쓰레드가 깨어나는데 동기화에 의해서 하나의 쓰레드를 제외한 나머지 쓰레드는 다시 대기 상태에 빠지게 된다.
그러므로 notifyAll()
을 사용해서 우선순위 등을 고려하는 JVM의 스케쥴러에게 쓰레드 할당을 위임하자