Java에서 Thread를 구현하는 방법은 크게 두 가지가 있다.
그 후 run() 메소드를 오버라이딩하는 것이다.
public class ThreadTest1 extends Thread {
@Override
public void run(){
// 작업 내용
}
}
public class ThreadTest2 implements Runnable {
@Override
public void run(){
// 작업 내용
}
}
위 방법 중 Thread를 상속하는 방법은 다른 클래스를 상속받을 수 없기에(*Java는 하나의 클래스만 상속 가능) Runnable 인터페이스를 구현하는 방법을 좀 더 권장하는 편이다.
위처럼 만든 Thread 자식 클래스 또는 Runnable 인터페이스 구현 클래스를 이용해 실제 실행 흐름을 생성하는 방법에 대해 알아보자.
public class ThreadTest1 extends Thread {
@Override
public void run(){
// ...
}
public static void main(String[] args){
Thread th = new ThreadTest1();
th.start();
}
}
public class ThreadTest2 implements Runnable {
@Override
public void run(){
// ...
}
public static void main(String[] args){
Runnable r = new ThreadTest2();
Thread th = new Thread(r);
th.start();
}
}
Thread를 상속한 클래스의 경우 바로 new 키워드를 이용해 새로운 스레드 객체를 생성할 수 있다.
Runnable 인터페이스를 상속한 클래스의 경우 새로운 Thread의 인자로 인스턴스를 생성해 넘겨줘야 한다.
위와 같이 스레드를 구현해야 하는 이유는 Thread 클래스의 내부 구조가 다음과 같기 때문이다.
private Runnable target;
...
@Override
public void run() {
if (target != null) {
target.run();
}
}
Thread를 상속받아 run() 메소드를 오버라이딩하면 해당 메소드 로직이 변경되고,
Runnable 구현 클래스를 만들어 Thread 생성자로 인스턴스를 넘겨주면 위의 코드에서 target 변수에 해당 인스턴스가 담겨 스레드 실행시 Runnable의 run() 메소드가 호출된다.
위의 예시 코드에서는 Thread 객체를 생성 후 start() 메소드를 호출해 실행 흐름을 생성했다.
이때 run() 메소드를 사용하면 안되는 걸까?
안 된다.
둘의 차이는 다음과 같다.
run() 메소드는 일반 메소드처럼 작용하며 독립적인 실행 흐름이 생성되지 않는다.
start() 메소드는 별개의 새로운 실행 흐름(스레드)을 생성한다.
아래 예시 코드를 보자.
public class ThreadTest2 implements Runnable {
@Override
public void run(){
while(true){
System.out.println("Running");
sleep(100);
}
}
public static void main(String[] args){
Runnable r = new ThreadTest2();
Thread th = new Thread(r);
th.run();
System.out.println("Terminated");
}
}
위와 같이 run() 호출 시 "Running"만 무한히 찍히며 "Terminated"는 출력되지 않는다.
public class ThreadTest2 implements Runnable {
@Override
public void run(){
while(true){
System.out.println("Running");
sleep(100);
}
}
public static void main(String[] args){
Runnable r = new ThreadTest2();
Thread th = new Thread(r);
th.start();
System.out.println("Terminated");
}
}
단, 위와 같이 start() 호출 시 "Terminated"가 출력되는 것을 볼 수 있다.
이는 start()로 호출 시 이를 호출한 스레드(부모 스레드)와 호출로 생성된 스레드(자식 스레드)의 실행 흐름이 독립적으로 흘러가기 때문이다.
단, run()을 호출 시 main 스레드는 해당 메소드가 수행되기를 기다린 뒤 남은 작업을 이어서 진행하게 된다.
(출처: https://mangkyu.tistory.com/309)
아래와 같이 Thread는 상태가 변화한다.
위에서 Wating/Blocked 상태가 되는 상태 제어 메소드와 다시 Runnable 상태로 돌아가는 메소드들에 대해 좀 더 자세히 알아보자.
sleep()은 알다시피 인자로 넘겨진 시간(ms)만큼 일시정지한다.
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
// interrupt() 메소드가 호출되면 실행
}
일시 정지 상태에서 interrupt()가 호출되면 InterruptedException가 발생한다.
interrupt() 메서드는 작업 취소 요청을 보내는 역할이다.
interrupt()가 호출되는 경우 isInterrupted() 반환값이 true가 된다.
이 때 isInterrupted()는 결과를 반환하고 false로 초기화한다.
따라서, true는 interrupt()가 호출된 후 최초로 호출된 isInterrupted()에 한정해서 반환된다.
public class ThreadTest2 implements Runnable {
@Override
public void run(){
while(!isInterrupted()){
System.out.println("Running");
sleep(100);
}
System.out.println("Terminated");
}
public static void main(String[] args){
Runnable r = new ThreadTest2();
Thread th = new Thread(r);
th.start();
sleep(1000);
try{
th.interrupt();
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
위와 같은 경우 Running이 열번만 출력된 후 Terminated가 출력될 것이다.
(만약 sleep 중 interrupt가 호출되면 InterruptedException가 발생하여 Terminated는 출력되지 않음)
join()은 다른 스레드가 종료될 때까지 대기하는 메소드이다.
public class ThreadTest2 implements Runnable {
@Override
public void run(){
try{
while(!isInterrupted()){
System.out.println("Running");
sleep(100);
}
} catch(InterruptedException e) {
e.printStackTrace();
}
sleep(1000);
System.out.println("Terminating");
}
public static void main(String[] args){
Runnable r = new ThreadTest2();
Thread th = new Thread(r);
th.start();
sleep(1000);
th.interrupt();
th.join();
System.out.println("Terminated");
}
}
위에서 th.join()이 빠지면 "Terminated" > "Terminating" 순으로 출력된다.
하지만 join()을 호출하면 th 스레드가 종료되기까지 대기하다 그 뒤를 수행하므로 "Terminating" > "Terminated" 순으로 출력이 이루어진다.
이외에도 wait()(일시정지), notify()(wait으로 대기 상태에 있는 스레드를 다시 수행 가능한 상태로 깨움), notifyAll()(특정 스레드 그룹에 있는 모든 스레드들을 notify), yield()(running 상태인 스레드를 runnable로 변경) 등이 있다.
멀티 스레드에서 스레드 동기화를 하는 목적은 "공유 자원을 보호"하기 위함이다.
공유 자원에 상호 배타적으로 접근하게 함으로써 공유 자원을 보호하고 동기화를 구현한다.
Thread의 동기화는 synchronized와 스레드 Lock을 이용해 작성할 수 있다.
synchronized는 메소드 synchronized와 블록 synchronized 두 가지로 작성할 수 있다.
public synchronized void function_name(){ //작업 내용 }
public class ThreadLock(){
private Object lockObj = new Object();
public void testLock(string name) {
synchronized(lockObj) { // 작업 내용 }
}
}
synchronized(String.class){ // 작업 내용 }