2023.02.02 - 안드로이드 앱개발자 과정

CHA·2023년 2월 4일
0

Java



Thread

작성된 코드를 실행하는 객체 : 동시에 여러작업을 처리하는 기법


Thread 란?

스레드란, 동시에 여러작업을 처리하고 싶을 때 사용하는 기법으로 작성된 코드를 실행하는 객체 입니다. 무엇인지 정확히 알아보기 이전에 간단한 용어 먼저 정리하겠습니다.

용어정리

process

실행중인 하나의 자바 프로그램. 프로세스 간에는 별도의 메모리를 사용하므로 자원 공유가 불가합니다.

Thread

하나의 프로세스 안에서 동작하는 직원같은 개념입니다. 스레드간에는 같은 메모리 공간을 사용하므로 자원 공유가 가능합니다.

Main Thread

하나의 프로세스에는 반드시 하나의 스레드가 존재합니다. 그 스레드를 Main Thread 라고 합니다.

만일, 메인스레드가 시간이 오래 걸리는 작업을 수행한다고 합시다. 그러면 메인스레드는 그 작업 때문에 다른 작업을 하지 못하게 됩니다. 그렇기 때문에 다른 작업을 처리하기 위해서 별도의 스레드 객체를 만들어 사용해야 합니다.

스레드를 사용해보기 이전에, 파일 다운로드와 음악 재생에 관한 예제를 스레드를 사용하지 않고 실행시켜보고 그 결과를 통해 스레드의 필요성을 알아봅시다. (실제 재생이나 파일다운로드를 하는건 아닙니다)

먼저, 파일 다운로드 코드 입니다. 스레드의 실행 느낌을 잘 느끼기 위해 코드 중간에 반복문을 돌려 실행을 늦춰줍시다.

for (int i = 0; i < 20; i++) {
	System.out.println(i+1 + " 번째 파일 다운로드 중...");
			
	for(long k = 0; k < 8000000000L ; k++) {
		new String("Hello");
	}
}

다음은 음악을 재생하는 코드입니다. 마찬가지로 반복문을 통해 실행을 늦춰줍시다.

for (int i = 0; i < 20; i++) {
	System.out.println(i+1 + " 번째 음악 재생 중...");
			

	for(long k = 0; k < 8000000000L ; k++) {
		new String("Hello");
	}
}

결과를 돌려보면 먼저 파일이 다 다운로드가 이뤄진 후에 음악재생 코드가 실행됩니다. 지금까지 우리가 해왔던 코드 실행은 모두 순차적으로 이루어지기 때문에 위 코드가 이상한 코드는 아닙니다.

다만, 우리가 일상생활에서 파일을 다운로드하고 음악을 재생시켜본다고 합시다. 그러면 좀 이상하지 않나요? 파일을 다운로드 하는 와중에 음악을 듣지, 파일 다운로드가 완료된 다음에 음악을 듣진 않으니까요. 그래서 우리는 다운로드와 음악재생을 동시에 하고 싶습니다. 그러기 위해서는 다른 작업을 동시에 처리할 수 있는 스레드가 필요합니다.

스레드 작업을 위해 우리는 별도의 A직원과 B직원을 채용하겠습니다. 이 직원들의 객체를 생성 해서 각각 음악 재생과 파일 다운로드를 해보도록 설계 하겠습니다. 즉, 별도의 class 를 설계하여 각 class 가 Thread 의 능력을 가지도록 하며 각 Thread가 해야할 작업들을 작성해 놓아야 합니다. 자 만들어봅시다.

---------------- ThreadA.java
// 1.
class ThreadA extends Thread {
	// 2.
    @Override
	public void run() {
    	// 6.
		String name = Thread.currentThread().getName();
		for(int i = 0; i < 20 ; i++) {
			System.out.println(name +" : " + (i+1) + " 번째 파일 다운로드 중...");
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {}
		}
	}
}
---------------- ThreadB.java
// 1.
class ThreadA extends Thread {
	// 2.
    @Override
	public void run() {
    	// 6.
		String name = Thread.currentThread().getName();
		for(int i = 0; i < 20 ; i++) {
			System.out.println(name +" : " + (i+1) + " 번째 파일 다운로드 중...");
			try {
            	// 3.
				Thread.sleep(500);
			} catch (InterruptedException e) {}
		}
	}
}

----------------- Main.java
public class Main {

	public static void main(String[] args) {
		// 4.
        ThreadA a = new ThreadA();
		// a.run(); error!
		a.start(); 
	
		ThreadB b = new ThreadB();
		b.start();
		
        // 5.
		for(int i = 100; i< 120; i++) {
			System.out.println(i);
            // 6.
			String name = Thread.currentThread().getName();
			try {
            	// 3.
				Thread.sleep(500);
			} catch (InterruptedException e) {}
		}
	}
}
  1. Thread 를 상속하는 클래스 ThreadAThreadB 를 만들어줍니다.

  2. Thread 클래스에 있는 run() 메소드를 오버라이드 해줍니다. 모든 Thread 는 이 run() 메소드 안에 작성한 부분만 스스로 실행합니다. 그 외의 부분은 Main Thread 가 실행하는 구역입니다.

  3. Thread.sleep(500); 을 통해 스레드를 0.5 초 일시정시 시킵니다. sleep() 은 static 메서드인데, 앞쪽에 Thread. 으로 어떤 클래스의 메소드인지 명시해 주는것이 좋습니다. 또한 단위는 밀리세컨드로, 500을 파라미터로 전달하면 0.5 초동안 일시정지가 가능합니다. 추가적으로 sleep() 호출로 일시정지된 스레드를 강제로 깨우기 위해서는 interrupt() 메서드를 사용하면 스레드는 깨어나게 됩니다. 위 코드에서는 Thread 의 느낌을 천천히 느껴보기 위한 코드입니다.

  4. Thread 의 객체를 생성합니다. 그리고 a.start() 를 통해 run() 을 실행시켜 줍시다. 단, 주의할 점은 Thread 가 해야할 작업이 명시되어 있는 run() 메소드를 Main Thread 에서 직접 호출해서는 안됩니다. 직접 호출할 경우 Main Thread 에서 작업을 처리하기 때문에 Thread 에게 작업을 맡기고 싶다면 start() 메서드를 통해서 run() 을 호출해주어야 합니다. start() 메서드를 호출하면 run() 은 자동적으로 호출됩니다.

  5. Main Thread 또한 추가적인 작업이 가능합니다. 위 코드에서 a.start()b.start() 를 통해 스레드를 실행시키고 난 뒤 Main Thread 는 정상적으로 코드진행을 이어나갑니다. 그렇기 때문에 추가적인 작업이 가능합니다.

  6. Thread 클래스의 static 메소드인 currentThread() 를 통해 현재 스레드 객체의 이름을 얻어올 수 있습니다. 즉, Thread.currentThread() 의 리턴값은 현재 스레드 객체이며 그 객체가 가지고 있는 getName() 메서드를 통해 객체의 이름을 반환합니다.

  7. 위 코드에는 작성하지 않았지만, setPriority() 메서드가 있습니다. Thread 클래스에 있는 메서드로, 스레드가 실행될 우선순위를 정할 수 있습니다. 매개변수로 0부터 10까지의 정수값을 넣는데, 10에 가까울 수록 우선순위가 커집니다. 하지만 100프로는 아니고 상황에 따라 달라질 수는 있습니다. 즉, 우선순위가 될 확률을 높여주는 메서드라고 생각하면 됩니다.


Runnable 인터페이스

Thread 능력, 즉 코드를 실행할 수 있는 능력을 가지는 2가지 방법이 있습니다.

방법 1
앞서 보았듯 Thread 클래스를 상속받은 클래스를 하나 설계하고 객체를 생성하여 start() 메서드를 이용하는 방법입니다.

방법 2
이제 보게 될 Runnable 인터페이스를 활용하는 방법입니다.

여기서 주목해야할 점은 Runnable 이 인터페이스 라는 점입니다. 그렇기에 Thread 를 이용함과 동시에 다른 클래스를 상속받아 다중상속의 효과도 노려볼 수 있습니다. 이것과 함께 Runnable 인터페이스의 사용방법을 봅시다.

------------- Person.java
class Person {
	String name;
	int age;
}

------------- PersonThread.java
class PersonThread extends Person implements Runnable{
	
    @Override
	public void run() {
		for( int i = 0; i<5; i++) {
			System.out.println(name + ", " + age);
			
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {}
		}
	}
}

------------- Main.java
public class Main {
	public static void main(String[] args) {
    	PersonThread pt = new PersonThread();
        // pt.start(); //error!
        Thread t = new Thread(pt);
        t.start();
    }
}

위 코드는 Person 클래스를 설계했으며, Person 클래스를 상속하고 Runnable 인터페이스를 구현하는 클래스 PersonThread 를 설계했습니다. 이를 이용하여 스레드 작업을 하는 코드 입니다.

위 코드에서 유의깊게 볼 부분은 pt.start(); 입니다. 이렇게 호출하면 에러가 나는데, 이는 Runnable 이 인터페이스 이기 때문입니다. 앞서 Thread 클래스를 상속받아 스레드 작업을 시작할 때 .start() 를 통해 시작할 수 있는 반면에, 여기서는 Runnable 이 인터페이스 이기 때문에 바로 .start() 를 이용하여 스레드를 시작할 수 없습니다. Thread 클래스 처럼 기능이 작성되어 있지 않고 추상메소드의 형태로 존재하기 때문입니다. 그래서 우리는 Runnable 인터페이스를 구현한 객체를 Thread 객체로 변환 시켜준 뒤, start() 를 호출해주어야 합니다.

즉, 다음과 같습니다.

---------------- Main.java
Thread t = new Thread(pt);
t.start();

Thread 의 객체를 생성하였으며, 생성자로 PersonThread 객체의 참조변수를 넘겨주었습니다. 그래서 스레드를 실행시킬 수 있게되었죠. 그런데 보시다시피 기존 Thread 를 이용한 방식보다 좀 더 복잡합니다.

원래 Thread 클래스는 Runnable 인터페이스를 활용할 때의 트리거로써 만들어진 클래스 입니다. 위처럼 사용하기 위해서요. 그런데 이 방식이 비효율적이라 느끼게 되고 Thread 클래스 자체에서 스레드를 실행시킬 수 있는 방식으로 발전하게 된것입니다.


익명클래스 (Anonymous Class)

물론, 위처럼 다중상속의 효과를 위한게 아닌 스레드 객체만 필요할 때도 Runnable로 만들기도 합니다. 다만 Runnable은 인터페이스 이기 때문에 별도의 클래스를 생성한 후 객체로 생성해주어야 합니다.

그런데 이런식으로 별도의 클래스를 생성하자니 이름 명명하는것 또한 은근 스트레스 입니다. 이름에 따라서는 이 클래스가 Thread 를 구현한 클래스인지 아닌지도 판단하기 어렵기 때문이죠. 또한 Thread 를 구현한 클래스와 start() 를 호출하는 클래스가 코드 상에서 너무 멀리 떨어지게 되는 경우가 많다보니 한눈에 들어오지 않는 단점이 생기게 됩니다.

그래서 자바에서는 스레드 작업코드를 작성하는 run() 메소드를 스레드 객체를 생성하면서 곧바로 작성할 수 있는 문법을 만들었습니다. 이것이 익명클래스 입니다. 사용법은 다음 코드를 봅시다.

Runnable r = new Runnable() {
	@Override
	public void run() {
		System.out.println("별도 스레드가 작업할 내용...");
	}			
};

new Thread(r).start();

정리해보면, 원래 Runnable 인터페이스로 스레드를 사용하기 위해서는 Runnable 를 구현한 클래스가 별도로 필요합니다. 추상메소드로만 이루어진 인터페이스로는 메소드 호출이 불가능하기 때문입니다.

그리고 그 클래스를 설계할 때 run() 메소드를 오버라이드 하여 재정의 한 뒤, run() 메소드를 호출하기 위해서 클래스의 객체를 Thread 로 변환하는 작업이 필요합니다. 그리고 변환된 스레드 객체를 통해 start() 메서드를 호출하여 스레드를 실행해주어야 합니다. 이렇게 복잡한 과정을 익명클래스를 이용하면 위와 같이 간단하게 끝이 납니다.

다만, 익명클래스는 일회성 클래스 입니다. 클래스를 설계하는 목적 중 하나는 재사용을 하기 위함인데, 익명클래스는 재사용이 불가합니다. 그래서 단발성으로 사용할 스레드일 경우에 사용하는것이 좋습니다.

또한 익명클래스를 스레드 파트에서 소개하긴 했지만, 다른 경우에서도 사용은 가능합니다. 재사용이 필요없거나 메서드를 오버라이드 해야할 때 한눈에 보고싶다면 익명클래스를 통해 사용하면 편리합니다.



Thread 맘대로 휘두르기

  • 동기와 비동기 ( Synchronized & Asynchronized )

  • sleep(), interrupt()

  • join()

  • wait(), notify()

    Thread 가 어떠한 작업을 하는 클래스인지, 사용법은 무엇인지에 대해 알아보았습니다. 이제 Thread 를 더 잘 제어하기 위해서 어떠한 기능들이 있는지에 대해 살펴봅시다. 먼저 동기와 비동기 부터 알아보도록 하겠습니다.

동기와 비동기는 일상생활에서도 많이 들었을 법한 용어인데, 막상 생각해보면 어떠한 의미인지 헷갈립니다. 그러니 동기와 비동기의 용어부터 정리하고 시작해봅시다.

용어정리

1. 동기화 처리

Thread 를 사용하는 것은 비동기 처리를 의미합니다. 이로 인한 문제를 처리하는 기법을 동기화 처리 라고 합니다.

2. 동기 (Synchronized)

A 작업이 끝나면 B 작업을 시작하는것이라고 생각합시다. 원래 동기란 데이터의 요청과 동시에 결과가 나와야 한다는 의미입니다. 즉, 데이터를 요청하는 작업외에 다른 작업은 할 수 없음을 의미합니다.

3. 비동기 (Asynchronized)

비동기는 동기와 반대의미라고 생각하면 좋습니다. A의 작업과는 관계없이 B 작업을 시작하고 끝낼 수 있습니다. 비동기란 데이터의 요청과 결과는 동시에 일어나지 않는다는 의미입니다. 즉, 데이터의 요청외에 다른 작업을 할 수 있음을 의미합니다.


동기화 예제 - 동기화의 필요성

자 그러면, 동기와 비동기에 관련한 예제 하나를 해봅시다. 계좌 입금 예제입니다.

------------------ Main.java
public class Main {
	public static void main(String[] args) {
    	Account acc = new Account();
        
        TestThread t1 = new TestThread(acc);
        TestThread t2 = new TestThread(acc);
        t1.start();
        t2.start();
    }
}
------------------ Account.java
public class Account {
	int money = 0;
    
    public void add(int money) {
    	String name = Thread.currentThread().getName();
        System.out.println(name + " : " + "입금 작업을 시작합니다.");
        System.out.println(name + " : " + "현재 잔액 : " + this.money);
        this.money += money;
    }
    
		for(long i = 0; i < 60000000000L; i++) {
			new String();
		}
		System.out.println(name + " : " + "입금 후 잔액 : " + money);
}
------------------ TestThread.java
public class TestThread {
	
    Account acc;
    
    public TestThread(Account acc) {
    	this.acc = acc;	
    }
    
    @Override
	public void run() {
		acc.add(100);
	}
}

add() 기능이 있는 Account 클래스 하나와, Thread 기능을 수행할 TestThread 클래스 하나를 설계했습니다. 중간에 실행결과를 잘 확인할 수 있도록 for문을 작성해주었으며, TestThread 생성자를 통해 Account 의 객체를 넘겨주어 TestThread의 run() 메소드에서 add() 기능을 수행하도록 설계했습니다.

우리는 100원씩 입금을 하는 기능을 2번 실행했습니다. 서로 다른 스레드를 통해서요. 그런데 문제는 실행결과입니다. 결과를 보면 입금 후 잔액이 100원 입니다. 분명 2번을 실행했으면 200원이 되어야하는데 말이죠.

이는 하나의 스레드가 기능을 수행하는 도중, 다른 스레드가 기능을 실행해버려 제대로 기능 수행이 안되었다는 이야기죠. 그래서 우리는 한 스레드가 끝난 뒤, 다음 스레드를 실행시키고 싶습니다. 입금을 동시에 하게 만들지 않기 위해서요.


동기화 예제 - 동기화 처리 ( Synchronized )

그래서 다음과 같은 동기화 처리를 해주겠습니다. 코드를 먼저 봅시다.

synchronized void add(int money) {
	String name = Thread.currentThread().getName();
	System.out.println(name + " : " + "입금 작업을 시작합니다.");
	System.out.println(name + " : " + "현재 잔액 : " + this.money);
	this.money += money;

	for(long i = 0; i < 60000000000L; i++) {
		new String();
	}
	System.out.println(name + " : " + "입금 후 잔액 : " + this.money);
}

이번에 출력결과를 확인해보면 조금 달라졌습니다. 앞선 코드에서는 두개의 스레드 객체가 거의 동시에 작업을 시작하고 끝냈다면, 이번에는 먼저 시작한 스레드 객체의 작업이 끝난 다음, 나머지 스레드 객체가 작업을 시작하고 끝냅니다.

이런 결과가 나올 수 있는 이유는 synchronized 키워드 덕분입니다. 키워드를 메서드 앞에 붙여주면 메서드를 동기화 처리할 수 있습니다. 이렇게 메서드를 동기화 처리를 하게 되면 이 메서드는 하나의 스레드만 점유할 수 있게됩니다. 즉, 하나의 스레드가 이 기능을 수행하고 있는 도중에 다른 스레드가 이 기능을 실행할 수 없습니다. 입금 후 잔액을 보아도 정상적으로 200원이 채워져있음을 알 수 있습니다.

단, 주의할 점은 동기화처리를 어떠한 기능에 부여할지를 잘 생각해야한다는 점입니다. 여기에서는 입금기능 을 동시에 사용하면 안되기 때문에 add() 메서드 앞쪽에 synchronized 키워드를 이용하여 동기화처리를 해주었습니다.


동기화 예제 - 동기화 블럭

그런데 이렇게 잘 마무리 되나 싶었는데 아직 문제가 남았습니다. 예를 들어 ATM 기에서 입금 기능 버튼을 눌렀다고 해봅시다. 그런데 이미 그 기능은 사용중인 상황입니다. 그러면 지금 내가 사용하고 있는 이 ATM 기에서는 아무런 반응도 할 수 없습니다. 입금 기능은 이미 다른 스레드에서 사용중인 기능이고, 그 기능은 동기화가 되어있어 기능을 실행할 수 없는것이죠.

잠깐만.. 혹시 그러면 입금 기능 버튼은 눌리되, 안내 문구 정도만 띄워놓고 실제 입금 작업만 동기화처리를 할수는 없을까요? 그렇게 되면 사용자 입장에서 볼 때, 입금 버튼은 눌렸고 기능이 잘 실행된다고 느낄테니 말입니다. 이처럼 부분적인 동기화 또한 가능합니다. 다음 코드를 봅시다.

void add(int money) {
	String name = Thread.currentThread().getName();
	System.out.println(name + " : " + "입금 작업을 시작합니다.");
		
		
	synchronized (this) {
		System.out.println(name + " : " + "현재 잔액 : " + this.money);
		this.money += money;
			
		for(long i = 0; i < 60000000000L; i++) {
				new String();
		}
		System.out.println(name + " : " + "입금 후 잔액 : " + this.money);
			
		System.out.println();
	}
}

앞서 synchronize 키워드를 통해 메소드 앞에 키워드를 붙여, 메소드 자체를 동기화 처리 하였습니다. 그랬더니 앞서 이야기했던 문제가 발생할 수 있었죠. 그래서 부분적인 동기화를 위한 동기화 블럭을 만들어, 그 안에 동기화 처리를 할 코드를 작성했습니다.

위 코드는 입금작업을 시작한다는 안내문구는 동기화 처리를 하지 않았으며, 현재 잔액 표시와 실제 입금처리 과정, 입금후 잔액을 표시하는 코드들은 모두 동기화처리된 코드입니다. 실제로 실행결과를 보면, 시작한다는 안내문구 까지는 두 스레드 모두 동시에 처리하는 모습을 보이며, 블럭 안에 있는 부분은 한 스레드 객체가 끝날 때까지 다른 스레드 객체는 실행하지 않는 결과를 볼 수 있습니다.


sleep() 과 interrupt()

앞선 Thread 예제에서 배워보았던 내용입니다. sleep() 을 이용하면 Thread 의 진행을 잠시동안 멈추게 할 수 있습니다. 반대로, interrupt() 를 이용하면 멈춰있던 Thread 의 진행을 재개할 수 있게 됩니다.


join()

자, A 라는 스레드에게 작업을 의뢰해봅시다. 그런데 다른 스레드 B가 이 타이밍에 작업을 시작하고 싶다고 합니다. 그런데 우리가 생각했을 때에는 A 작업이 끝난 뒤에 B 작업을 시작해야 합니다. 아무런 조치도 하지 않는다면 분명히 A 작업과 B 작업은 동시에 일어나게 될껍니다. 이런 상황에서는 어떻게 처리를 해야할까요? 이 예시는 분명 앞선 예제들과는 차이가 있습니다. 앞선 예제들에서는 ' 같은 작업 ' 을 대상으로 Thread 의 순서를 정했다면, 이번에는 각기 ' 다른 작업 ' 을 하고 있는 Thread 들의 순서를 정해주어야 합니다.

예를 하나 들어볼까요? 당근마켓으로 한번 생각해봅시다. 당근마켓은 위치 서비스를 기반으로 한 중고거래 플랫폼 이죠? 자 그럼, 당근마켓을 킵시다. 그러면 내 위치를 기반으로 게시글을 검색할 수 있습니다. 이때, 내 위치가 기반이 되어야 그 위치에 맞는 게시글을 띄워줄 수 있습니다. 그래야 당신 근처의 중고마켓이 될테니까요. 그러려면 일단 내 위치를 가져오는 작업을 우선 마무리 하고 그 뒤에 그 가져온 위치를 기반으로 게시글을 띄워야 하겠죠?

즉, A 스레드 에서 위치를 받아오는 작업을 한다면 , B 스레드에서 게시글을 띄워주는 작업을 하는 상황인겁니다. 이럴때 사용하는 메서드가 join() 메서드 입니다.

--------------------- Main.java
public class Main {
	public static void main(String[] args) {
    	AThread at = new AThread();
        BThread bt = new BThread();
        
        at.start();
        
        try {
			at.join();
		} catch (InterruptedException e) {}
		
        bt.start();
    }
}
--------------------- AThread.java
class AThread extends Thread {
	@Override
	public void run() {
		System.out.println("A 스레드");
		try {
			Thread.sleep(500);
		} catch (InterruptedException e) {}
	}
}
--------------------- BThread.java
class BThread extends Thread {
	@Override
	public void run() {
		System.out.println("B 스레드");
		try {
			Thread.sleep(500);
		} catch (InterruptedException e) {}
	}
}

실행결과에서 볼 수 있듯, A 스레드의 작업이 모두 끝난후에야 B 스레드의 작업이 시작됩니다. 편리하죠?


wait() 와 notify()

타이어 조립 공장 예제를 통해 wait()notify() 를 알아보겠습니다. 먼저 CThread 클래스를 하나 설계해서 1~4 번 타이어를 조립하는 코드를 짜봅시다.

----------------- Main.java
public class Main {
	public static void main(String[] args) {
    	CThread ct = new CThread();
        ct.start();
    }
}
----------------- CThread.java
class CThread extends Thread {
		@Override
	public void run() {
		System.out.println("1번 타이어 조립");
		System.out.println("2번 타이어 조립");
		System.out.println("3번 타이어 조립");
		System.out.println("4번 타이어 조립");
        System.out.println("퇴근~");
	}
}

이렇게 짜놓고 보니 일을 얼마 안시키는 느낌입니다. 고작 타이어 4개만 조립하고 직원들을 퇴근시키게 생겼습니다. 그래서 우리는 직원들에게 일을 더 시킬껍니다. 그러면 어떤 방식으로 일을 더 시킬까요? 그 이전에 우리는 대전제가 필요합니다. 직원들은 run() 메소드가 끝나면 퇴근입니다. 그러면 앞서 일을 시킬때, start() 메서드를 호출 했으니, 한번더 호출하면 괜찮지않을까요? 에러가 날껍니다. Thread 는 run() 가 끝나면 더 이상 실행할 수 없기 때문입니다.

그러면 새로운 Thread 객체를 생성하고 start() 를 호출하는건 어떨까요? 이건 가능하긴 합니다. 실행은 가능하지만, Thread 의 비동기적 특성 때문에 타이어 1~4 번이 조립이 완료된 후 다음 작업을 수행하는것이 아니라, 동시에 작업이 진행되게 됩니다. 그렇기 때문에 우리가 원하던 그림은 아닌거죠.

그러면 시선을 돌려 run() 메소드 안으로 가봅시다. 이 run() 메소드 안에서 잘 조작하면 반복을 할 수 있을것 같네요. 자 가봅시다.

----------------- Main.java
public class Main {
	public static void main(String[] args) {
    	CThread ct = new CThread();
        ct.start();
        
        try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {}
        
        ct.stop();
    }
}
----------------- CThread.java
class CThread extends Thread {
	boolean isRun = true;
    boolean isWait = false;
    
	@Override
	public void run() {
    	while(isRun) {
          System.out.println("1번 타이어 조립");
          System.out.println("2번 타이어 조립");
          System.out.println("3번 타이어 조립");
          System.out.println("4번 타이어 조립");
          System.out.println("퇴근~");
       }
	}
}

자, 이렇게 해서 반복적으로 돌릴 수 있게 되었습니다. while문의 조건에 true 를 넣고 싶었지만 그러면 에러가 나기 때문에 boolean 형 변수 isRun 을 선언해준 뒤 넣어주었습니다.

이제 직원들도 할만큼 했으니 퇴근을 시켜야겠죠? Main Thread 에서 stop() 메서드를 통해 스레드를 멈추게해 퇴근시키는건 어떨까요? 사실, 이 방법은 권장되지 않습니다. 예를 들어, while 문을 계속 돌다가 중간에 stop() 을 만나게 되면 while 문을 돌던 중간에도 끝날 수가 있기 때문입니다. 즉, 1번과 2번타이어까지만 조립하고 3,4 번은 조립되지 않은채 끝날 수 있다는 이야기입니다. 다시 말해, 이는 자원에 대한 리소스들이 제대로 마무리 되지 않는다는 것이죠.

그러면 while 문의 조건을 변경하는게 좋지 않을까요? 맞습니다. 조건을 변경하면 깔끔하게 4번까지 조립하고 while 문이 종료되기 때문에 리소스 또한 제대로 마무리 될것 같습니다. 다만 그 방법이 중요합니다. 메인 클래스에서 직접 CThread 클래스에 접근해서 조건을 바꾸는건 좋지 않습니다. 객체지향적인 방법이 아니기 때문이죠. 그렇기 때문에 CThread 에서 조건을 바꾸는 기능메서드를 만들어 호출해주는 방법을 이용해주어야 합니다.

다음 코드를 보죠.

----------------- Main.java
public class Main {
	public static void main(String[] args) {
    	CThread ct = new CThread();
        ct.start();
        
        try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {}
        ct.pauseThread();
        ct.resumeThread();
        ct.stopThread();
    }
}
----------------- CThread.java
class CThread extends Thread {
	boolean isRun = true;
    boolean isWait = false;
    
	@Override
	public void run() {
    	while(isRun) {
          System.out.println("1번 타이어 조립");
          System.out.println("2번 타이어 조립");
          System.out.println("3번 타이어 조립");
          System.out.println("4번 타이어 조립");
          
          if(isWait) {
          	 try {
				synchronized (this) {
					wait();
				}
			 } catch (InterruptedException e) {}
          }
          
          System.out.println("퇴근~");
       }
	}
    
    public void stopThread() {
    	this.isRun = false;
        synchronized (this) {
			notify();
		}
    }
    public void pauseThread() {
    	this.isWait = true;
    }
    public void resumeThread() {
    	this.isWait = false;
        synchronized (this) {
			notify();
		}
        
    }
}

퇴근 시키는 기능인 stopThread() 메서드를 만드는 김에 휴식 시킬 수 있는 기능인 pauseThread() 와 다시 복귀하게 하는 기능인 resumeThread() 메서드도 만들었습니다. while 문 안쪽에는 if문을 넣고 조건으로 isWait 변수를 넣어주었습니다.

그리고 이 조건값은 pauseThread()resumeThread() 를 통해 true 가 될 수도 false 가 될 수도 있습니다. 그래서 true 라면 wait() 메서드를 실행하여 이 스레드를 잠시 멈출 수 있게 됩니다.

wait() 메서드를 실행하면 스레드의 자원들은 잠시동안 wait pool 이라는 메모리 공간으로 넘어가게 됩니다. 그런데 이때, wait() 의 실행과 동시에 notify() 메소드의 실행이 될 수도 있기 때문에 동기화(synchronized) 가 꼭 필요합니다. 그래서 동기화 블럭을 통해 처리해주었습니다. notify() 또한 마찬가지 인데, 명령 실행 도중 wait() 이 실행될 가능성이 있기 때문에 이 역시 동기화가 필요합니다.

또한 stopThread() 를 호출하게 되면 notify() 를 호출하여 쉬고 있는 직원들을 모두 깨워 퇴근시키도록 해주어야 합니다.

profile
Developer

0개의 댓글