1. 프로세스와 스레드
프로세스란 운영체제에서 실행 중인 하나의 어플리케이션을 의미합니다. 프로그램을 실행하면 운영체제로부터 실행에 필요한 메모리를 할당 받아 해당하는 코드를 실행하는 작업의 단위를 프로세스라고 합니다.
스레드는? 프로세스 안에서 독립적으로 실행되는 흐름의 단위에요. 그리고 또 하나의 프로세스가 여러 스레드를 실행하는 것을 멀티 스레드라고 합니다.
2. 스레드의 생성과 실행
스레드의 생성방법은 크게 2가지가 있습니다.
- Thread 클래스를 상속받아 생성
- Runnable 인터페이스를 구현하여 생성
2 - 1. Thread 클래스를 상속받아 생성
public class ThreadExam extends Thread{
@Override
public void run() {
// 실행할 코드
}
}
실행 방법은 간단합니다. Thread를 상속받은 클래스의 인스턴스를 생성하고 Thread의 start() 메소드를 호출하면 됩니다.
ThreadExam thread = new ThreadExam();
thread.start();
2 - 2. Runnable 인터페이스를 구현하여 생성
public class ImplementRunnable implements Runnable {
@Override
public void run() {
// 실행할 코드
}
}
실행 방법은 Thread 인스턴스를 생성할 때 생성자의 매개값으로 Runnable 구현 객체를 넘겨주고 생성한 Thread 인스턴스의 start() 메소드를 호출하면 됩니다.
ImplementRunnable runnable = new ImplementRunnable();
Thread thread = new Thread(runnable);
thread.start();
3. 공유 객체
공유 객체란 말 그대로 "공유하는 객체"를 뜻하는데요. 화장실이 하나 있고, 화장실을 이용하는 사람이 세명 있다고 하면 화장실을 [공유 객체] , 사람들을 [Thread] 라고 말할 수 있습니다. 하나의 객체를 여러 개의 쓰레드가 함께 사용하거나 공유한다는 것을 의미해요.
예를 들어, ItemBox라는 공유 객체가 세 개의 메소드를 가지고 있다고 해볼게요. 해당 메소드들은 1초 이하의 시간동안 열번 반복하며 특정 물건을 출력합니다.
[ 공유 객체 ]
public class ItemBox{
public void getItemA(){
for(int i=0; i<10; i++){
System.out.println("A상품 !!!");
try {
Thread.sleep((int)(Math.random() * 1000));
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public void getItemB(){
for(int i=0; i<10; i++){
System.out.println("B상품 !!!");
try {
Thread.sleep((int)(Math.random() * 1000));
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public void getItemC(){
for(int i=0; i<10; i++){
System.out.println("C상품 !!!");
try {
Thread.sleep((int)(Math.random() * 1000));
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
[ 스레드 ]
public class ItemPlayer extends Thread{
int type;
ItemBox itemBox;
public ItemPlayer(int type, ItemBox itemBox){
this.type = type;
this.itemBox = itemBox;
}
public void run() {
switch(type){
case 1 : itemBox.getItemA(); break;
case 2 : itemBox.getItemB(); break;
case 3 : itemBox.getItemC(); break;
}
}
}
ItemBox를 사용하는 ItemPlayer 3명을 ItemPlayer라는 쓰레드에 만들었습니다. 받아들이는 타입에 따라 ItemBox가 가지고 있는 메소드가 다르게 호출되도록 run() 메소드를 오버라이딩 했습니다.
[ 실행 코드 ]
public class itemBoxExam {
public static void main(String[] args) {
// MusicBox 인스턴스
ItemBox box = new ItemBox();
ItemPlayer kim = new ItemPlayer(1, box);
ItemPlayer park = new ItemPlayer(2, box);
ItemPlayer lee = new ItemPlayer(3, box);
// MusicPlayer쓰레드를 실행합니다.
kim.start();
lee.start();
kang.start();
}
}
[ 결과 ]
상품 A !!!
상품 B !!!
상품 A !!!
상품 C !!!
상품 B !!!
상품 B !!!
상품 C !!!
상품 A !!!
상품 B !!!
상품 C !!!
상품 A !!!
상품 B !!!
상품 A !!!
상품 C !!!
상품 C !!!
...
출력 문이 뒤섞여 출력되는 것을 확인할 수 있죠 ? 비동기적으로 동작한다고 생각할 수 있습니다.
그런데 만약, ItemBox 의 세 개의 메소드가 동시에 호출되면 박스가 찢어진다고 가정해보겠습니다. 그럼 동시에 세 개의 메소드를 실행하면 안되겠죠 ? 이럴땐 어떻게 해야할까요 ??
4. Synchronized 메소드
이를 해결할 수 있는 방법이 바로 Synchronized 키워드를 return 타입 앞에 사용하면 됩니다. Synchronized를 사용하면 하나의 메소드가 실행될 때, 다른 메소드가 실행되지 못하도록 대기를 시키고 해당 메소드가 모든 실행이 완료되면 대기하던 다른 메소드가 실행되도록 만들 수 있어요 !
public class ItemBox{
public synchronized void getItemA(){
for(int i=0; i<10; i++){
System.out.println("A상품 !!!");
try {
Thread.sleep((int)(Math.random() * 1000));
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
....
}
여러 쓰레드들이 공유객체를 사용할 때, 메소드에 synchronized 키워드가 붙어있다면 0.000000000001초라도 메소드가 먼저 실행되면 해당 객체의 사용권을 얻게 됩니다. 이런 사용권을 보통 모니터 락(Monitor Lock)이라고 해요.
모니터 락은 메소드 실행이 종료되거나 wait() 같은 메소드를 만나기 전까지는 끝나지 않습니다. 다른 쓰레드들은 모니터 락이 끝날때까지 기다리고 있다가 두번째 쓰레드가 메소드를 실행하면서 모니터 락을 얻게되고 나중에 실행되는 쓰레드는 가장 마지막으로 모니터 락을 얻게 됩니다.
public class ItemBox{
public synchronized void getItemA(){
for(int i=0; i<10; i++){
System.out.println("A상품 !!!");
try {
Thread.sleep((int)(Math.random() * 1000));
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public synchronized void getItemB(){
for(int i=0; i<10; i++){
System.out.println("B상품 !!!");
try {
Thread.sleep((int)(Math.random() * 1000));
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public void getItemC(){
for(int i=0; i<10; i++){
System.out.println("C상품 !!!");
try {
Thread.sleep((int)(Math.random() * 1000));
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
다음과 같이 만약 getItemC() 라는 메소드에만 synchronized 키워드를 제거한다면, getItemA()와 getItemB()는 모니터 락을 얻으며 순차적으로 실행되지만, getItemC() 메소드 같은 경우 중간에 비동기적으로 실행이 되게 됩니다.
4 - 1. Synchronized 블록
그런데, 메소드 자체에 synchronized 키워드를 붙이게 되면 메소드 코드가 길어지면서 마지막으로 대기하던 쓰레드는 실행까지 굉장히 오랜 시간을 대기하는 상황이 발생할 수 있습니다.
이런 문제를 조금이라도 해결하고자 메소드 전체에 synchronized를 붙이기 보단 동시에 실행되면 안된느 부분에다가만 synchronized 블록을 만드는 방법이 있습니다.
public class ItemBox{
public void getItemA(){
for(int i=0; i<10; i++){
synchronized(this){
System.out.println("A상품 !!!");
}
try {
Thread.sleep((int)(Math.random() * 1000));
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
...
}
위와 같이 특정 부분에만 synchronized 블록을 사용하게 되면 synchronized 블록이 종료될 때 대기하던 쓰레드가 실행되면서 다른 쓰레드들이 조금 더 빠르게 실행에 진입할 수 있게 되는 것이다.
5. 스레드 상태제어
- 실행 중인 스레드의 상태를 변경하는 것
- 메소드로 주로 제어
- interrupt(), sleep(), join(), wait(), yield(), notify(), notifyAll() 등의 메소드가 존재
- 이중 notify(), notifyAll(), wait() 메소드는 Object 클래스의 메소드이고 나머지는 Thread 클래스의 메소드
[ sleep ]
- 실행중인 스레드를 일시 정지
- 매개값으로 밀리초를 넣으주면 해당 시간동안 sleep() 메소드를 만나는 스레드는 일시 정지합니다.
- 일시정지 상태에서 interrupt() 메소드를 호출할 겨우 InterruptedException이 발생합니다.
try {
Thread.sleep(1000);
}catch(InterruptedException){
//
}
[ yield ]
- 타 스레드에 실행을 양보합니다.
- 스레드가 처리하는 반복작업을 위해 for문이나 while 문을 사용하는 경우가 많습니다.
public class ThreadA extends Thread {
public boolean stop = false;
public boolean work = true;
public void run() {
while(!stop){
if(work){
System.out.println("ThreadA 작업 내용");
}else {
Thread.yield();
}
}
System.out.println("ThreadA 종료");
}
}
- yield() 메소드를 호출하면 호출한 스레드는 실행대기상태로 돌아가고 동일한 우선순위 혹은 높은 우선순위를 갖는 다른 스레드가 실행 기회를 갖게 됩니다.
[ join ]
- 다른 스레드가 종료되어야 실행해야하는 스레드가 존재
- 계산 작업이 그 예시로, 계산하여 결과를 return 하는 스레드가 존재하면 그것을 출력하는 스레드가 필요합니다.
- 이 때, 출력 스레드가 먼저 수행되면 안되겠죠 ? 계산이 이루어지고 계산값을 출력해야 합니다.
public class SumThread extends Thread{
private long sum;
public long getSum() {
return sum;
}
public void setSum(long sum) {
this.sum = sum;
}
public void run() {
for(int i =1; i<=100; i++) {
sum+=i;
}
}
@Override
public String toString() {
return "SumThread [sum=" + sum + "]";
}
}
public class JoinExample {
public static void main(String[] args) {
SumThread sumThread = new SumThread();
sumThread.start();
try {
sumThread.join();//현재 스레드 기준 (이부분을 주석처리해서 결과를 비교해보세요)
} catch (Exception e) {
}
System.out.println("1~100 합 : "+sumThread.getSum());
}
}
[ wait(), notify(), notifyAll() ]
- 스레드간 협력 메소드
- 두 개의 스레드를 번갈아가면서 실행
- 핵심은 공유객체의 활용
- 두 스레드가 작업할 내용을 동기화 메소드로 구분합니다.
- 스레드1 작업완료 -> notify() 메소드 호출 -> (일시정지) 스레드 2 실행대기상태로 변경 -> 스레드 1은 wait() (일시정지 상태)
- 이들 메소드는 동기호 메소드 혹은 동기화 블록에서만 사용이 가능합니다.
//공유객체
public class WorkObject {
public synchronized void methodA() {
System.out.println("ThreadA의 methodA() 작업 실행");
notify(); //일시정지 상태에 있는 ThreadB를 실행대기 상태로 만듬.
try {
wait();//ThreadA를 일시정지 상태로 만듬.
} catch (Exception e) {
}
}
public synchronized void methodB() {
System.out.println("ThreadB의 methodB() 작업 실행");
notify(); //일시정지 상태에 있는 ThreadA를 실행대기 상태로 만듬.
try {
wait();//ThreadB를 일시정지 상태로 만듬.
} catch (Exception e) {
}
}
}
//Thread A
public class ThreadA extends Thread{
private WorkObject workObject;
public ThreadA(WorkObject workObject) {
this.workObject = workObject;
}
@Override
public void run() {
for(int i =0; i<10; i++) {
workObject.methodA();
}
}
}
//ThreadB
public class ThreadB extends Thread{
private WorkObject workObject;
public ThreadB(WorkObject workObject) {
this.workObject = workObject;
}
@Override
public void run() {
for(int i =0; i<10; i++) {
workObject.methodB();
}
}
}
//main 스레드
public class WaitNotifyExample {
public static void main(String[] args) {
WorkObject shareObject = new WorkObject(); //공유객체 생성
ThreadA threadA = new ThreadA(shareObject);
ThreadB threadB = new ThreadB(shareObject);//ThreadA와 ThreadB 생성
threadA.start();
threadB.start();
}
}
- 메인 스레드에서 공유 객체를 생성합니다.
- 각각의 스레드의 멤버변수로 초기화하고, 공유 객체의 methodA와 methodB를 사용합니다.
- methodA와 methodB는 번갈아가면서 실행되어야 합니다.
- 이 협력 개념에서 발전하여 유명한 자바 디자인 패턴인 "생산자 소비자 패턴"으로 연결됩니다.
[ interrupt() ]
- run() 메소드가 모두 실행되면 스레드는 종료됩니다.
- 기존의 stop() 이란 메소드가 제공되었으나 deprecated 되었습니다.
- why ? 스레드가 사용하던 자원이 문제가 될 가능성이 존재
- interrupt() 메소드를 이용해 자원도 해제하며 안전하게 종료할 수 있습니다.
public class PrintThread2 extends Thread{
public void run() {
try {
while(true) {
System.out.println("실행 중");
Thread.sleep(1);
}
} catch (InterruptedException e) {
System.out.println("interrupt() 실행");
}
System.out.println("자원 정리");
System.out.println("실행 종료");
}
}
//메인 스레드
public class InterruptExample {
public static void main(String[] args) {
Thread thread = new PrintThread2();
thread.start();
try {
Thread.sleep(1000);
} catch (Exception e) {
}
thread.interrupt();
}
}
- Thread.sleep(1) 코드로 한 번 일시정지 상태를 만들어주고, 메인스레드에서 interrupt() 메소드를 실행하고 먼저 종료하였기 때문에 이후 printThread2 스레드는 자원을 정리하는 코드를 실행하며 안전하게 종료하게 된다.
6. 데몬 스레드
- 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드입니다.
- 주 스레드가 종료되면 데몬스레드는 강제적으로 자동 종료됩니다.
- Java의 Garbage Collector가 대표적인 데몬 스레드라고 합니다 -> JVM이 종료되면 같이 종료되기 때문
- 현재 스레드에서 다른 스레드를 데몬 스레드로 만드려면 데몬 스레드가 될 스레드의 참조객체에서 setDaemon(true)를 호출하면 됩니다.
- 주의점은 데몬스레드의 스레드가 이미 start() 메소드를 호출한 상태라면 IllegalThreadStateException이 발생하기 떄문에 start() 메소드를 호출하기 전에 setDaemon(true)를 실행해야 합니다.
public class AutoSaveThread extends Thread{
public void save() {
System.out.println("작업 내용을 저장함");
}
@Override
public void run() {
while(true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("1");//여기실행안됨. exception 발생은 아님
e.printStackTrace();
break;
}
save();
}
}
}
//메인 스레드
public class DaemonExample {
public static void main(String[] args) {
AutoSaveThread autoSaveThread = new AutoSaveThread();
autoSaveThread.setDaemon(true);
autoSaveThread.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
System.out.println("메인 스레드 종료");
}
}
- 코드를 실행하면 기존의 스레드는 메인 스레드가 죽어도 반복작업을 하는 경우에는 작업 스레드는 살아있어 프로그램이 죽지 않았습니다.
- 위 예제는 메인 스레드가 죽으면서 데몬스레드도 같이 죽어서 프로그램을 종료시킵니다.
<< 참고 자료 >>
자바 중급
현재 IOS/안드로이드 앱 내에서는 결제를 지원하지 않습니다.
school.programmers.co.kr
[Java] Thread#4, 스레드의 상태제어 메소드 및 데몬 스레드 개념 및 예제
스레드 상태제어 - 실행중인 스레드의 상태를 변경하는 것. - 메소드로 주로 제어 - interrupt(), sleep(), join(), wait(), yield() notify(), notifyAll() 등의 메소드가 존재. - 이중 notify(), notifyAll(), wait() 메소드
sas-study.tistory.com