발걸음/일지

[JAVA]Thread

리꾸므 2022. 11. 15. 18:05

Thread

  프로세스는 데이터, 컴퓨터 자원, 그리고 스레드로 구성되는데, 스레드는 데이터와 애플리케이션이 확보한 자원을 활용하여 소스 코드를 실행한다. 즉 스레드는 하나의 코드 실행 흐름으로 볼 수 있다.

 

 메인 스레드

  자바 애플리케이션을 실행하면 가장 먼저 실행되는 메서드 main 메서드이며, 메인 스레드가 main메서드를 실행시킨다. 처음부터 끝까지 순차적으로 실행시키며, 코드의 끝을 만나거나 return문을 만나면 실행을 종료한다.

 

 멀티 스레드

  하나의 프로세스는 여러 개의 스레드를 가질 수 있으며 이를 멀티 스레드 프로세스라고 말한다. 여러 스레드가 동시에 작업을 수행할 수 있으며 이를 멀티 스레딩이라고 한다.

 

 

멀티 스레드 프로세스

 


 

작업 스레드 생성과 실행

 메인 외 별도 작업 스레드 활용한다는 것은 작업 스레드가 수행할 코드를 작성하고 작업 스레드를 생성하여 실행시키는 것을 의미한다.

  • 자바는 객체지향 언어로 코드는 클래스 안에 작성된다. 따라서 스레드가 수행할 코드도 클래스 내부에 작성하고 'run()'이라는 메서드 내에 스레드가 처리할 작업을 작성하도록 되어 있다.
  • 'run()'메서드는 Runnabl 인터페이스와 Thread 클래스에 정의되어있다. 작업스레드를 생성하고 실행하는 방법은 크게 두 가지이다.
    1. Runnable 인터페이스를 구현한 객체에서 'run()'을 구현하여 스레드를 생성하고 실행하는 방법
    2. Thread 클래스를 상속받은 하위 클래스에서 'run()'을 구현하여 스레드를 생성하고 실행하는 방법

 

 1. Runnable 인터페이스를 구현한 객체에서 'run()'을 구현하여 스레드를 생성하고 실행

public class ThreadExample1 {
    public static void main(String[] args) {
    	//Runnable 인터페이스를 구현한 객체 생성
        Runnable test1 = new ThreadTask1();
        //Runnable 구현 객체를 인자로 전달하면서 Thread 클래스를 인스턴스화하여 스레드 생성
        Thread thread1 = new Thread(task1);
		
        //위의 두줄을 아래와 같이 한줄로 축약할 수도 있다.
        //Thread thread1 = new Thread(test1);
        
        //작업 스레드를 실행시켜, run() 내부 코드를 처리하도록 합니다.
        thread1.start();

        // 반복문 추가
        for (int i = 0; i < 100; i++) {
            System.out.print("@");
        }
    }
}
//Runnable 인터페이스 구현한 클래스
class ThreadTest1 implements Runnable {
    //메서드 바디에 스레드가 수행할 작업 내용 작성
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print("#");
        }
    }
}

//출력 결과,메인 반복문은 @, 작업 스레드 반복문은 #을 출력, 병렬 실행
@@@@@@@@@@@######@@@@@############################
@#########@@@@@@@@@@@@@@@@############@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@##@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@###########################################
Process finished with exit code 0

 

 2. Thread 클래스를 상속받은 하위 클래스에서 run()을 구현하여 스레드를 생성하고 실행

public class ThreadExample2 {
    public static void main(String[] args) {
		
        // Thread 클래스를 상속받은 클래스를 인스턴스화하여 스레드 생성
        Threadtest2 thread2 = new ThreadTest2();

        // 작업 스레드를 실행시켜, run() 내부의 코드를 처리하도록 합니다. 
        thread2.start();

        // 반복문 추가
        for (int i = 0; i < 100; i++) {
            System.out.print("@");
        }
    }
}

//Thread 클래스를 상속받는 클래스 작성
class ThreadTest2 extends Thread {
    
    //run() 메서드 바디에 스레드가 수행할 작업 내용 작성
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print("#");
        }
    }
}

 

 익명 객체를 사용하여 스레드 생성하고 실행하기

  클래스를 따로 정의하지 않고도 익명 객체를 활용하여 스레드를 실행시킬 수 있다.

 

  1. Runnable 익명 구현 객체를 활용한 스레드 생성 및 실행

public class ThreadExample1 {
    public static void main(String[] args) {
				
        // 익명 Runnable 구현 객체를 활용하여 스레드 생성
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.print("#");
                }
            }
        });

        thread1.start();

        for (int i = 0; i < 100; i++) {
            System.out.print("@");
        }
    }
}

 

  2. Thread 익명 하위 객체를 활용한 스레드 생성 및 실행

public class ThreadExample2 {
    public static void main(String[] args) {

        // 익명 Thread 하위 객체를 활용한 스레드 생성
        Thread thread2 = new Thread() {
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.print("#");
                }
            }
        };

        thread2.start();

        for (int i = 0; i < 100; i++) {
            System.out.print("@");
        }
    }
}

 

 


 

스레드의 이름

 메인 스레드는 'main' 추가적으로 생성한 스레드는 기본적으로 'Thread-n'이라는 이름을 가진다. 

 

  스레드의 이름 조회하기

   스레드의 이름은 '스레드의_참조값.getName()'로 조회 할 수 있다.

public class ThreadExample3 {
    public static void main(String[] args) {

        Thread thread3 = new Thread(new Runnable() {
            public void run() {
                System.out.println("Get Thread Name");
            }
        });

        thread3.start();

        System.out.println("thread3.getName() = " + thread3.getName());
    }
}

//출력 결과
Get Thread Name
thread3.getName() = Thread-0

Process finished with exit code 0

 

  스레드의 이름 설정하기

   스레드의 이름은 '스레드의_참조값.setName()'로 조회 할 수 있다.

public class ThreadExample4 {
    public static void main(String[] args) {

        Thread thread4 = new Thread(new Runnable() {
            public void run() {
                System.out.println("Set And Get Thread Name");
            }
        });

        thread4.start();

        System.out.println("thread4.getName() = " + thread4.getName());

        thread4.setName("Code States");

        System.out.println("thread4.getName() = " + thread4.getName());
    }
}

//출력 결과
Set And Get Thread Name
thread4.getName() = Thread-0
thread4.getName() = Code States

Process finished with exit code 0

 

 

  스레드 인스턴스의 주소값 얻기

   스레드의 이름을 조회하고 설정하는 메서드는 모두 Thread 클래스로부터 인스턴스화된 인스턴스의 메서드이므로 호출할 때 스레드 객체의 참조가 필요하다. 실행중인 스레드의 주소값을 사용해야 하는 상황이 발생시 정적 메서드인 'currentThread()'를 사용하면 된다.

public class ThreadExample1 {
    public static void main(String[] args) {

        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        });

        thread1.start();
        System.out.println(Thread.currentThread().getName());
    }
}

//출력 결과
main
Thread-0

Process finished with exit code 0

 

 


 

스레드의 동기화

  스레드 동기화란?

   앞서 프로세스는 자원, 데이터, 스레드로 구성된다. 프로세스는 스레드가 운영체제로부터 자원을 할당 받아 소스 코드를 실행하여 데이터를 처리한다. 싱글 스레드 프로세스 데이터에 단 하나의 스레드만 접근하므로, 문제될 사항이 없으나 멀티 스레드 프로세스의 경우, 두 스레드가 동일한 데이터를 공유하게 되어 문제가 발생 할 수 있다.

 

  • try { Thread.sleep(1000); } catch (Exception error) {}
    • Thread.sleep(1000);
      • 스레드를 일시 정지시키는 메서드, 어떤 스레드가 일시 정지되면 대기열에서 기다리고 있던 다른 스레드가 실행된다.
      • 또한, Thread.sleep()은 반드시 try...catch문의 try 블럭 내에 작성해야한다.
    • try {...} catch ( ~ ) { ... }
      • 'Thread.sleep(1000);' 동작을 위해 형식적으로 사용한 문법요소로 큰 의미를 두지 않는다.

 

  임계 영역(Critical section)과 락(Lock)

   임계 영역은 오로지 하나의 스레드만 코드를 실행할 수 있는 코드 영역을 의미하며, 락은 임계 영역을 포함하고 있는 객체에 접근 할 수 있는 권한을 의미한다.

  • 임계영역으로 설정된 객체가 다른 스레드에 의해 작업이 이루어지지않을때, 임의의 스레드 A는 해당 객체에 대한 락을 획득하여 임계영역 내의 코드를 실행 할 수 있다.
  • 스레드 A가 임계 영역내의 코드를 실행 중일 떄에는 다른 스레드들은 락이 없으므로 이 객체의 임계 영역 내의 코드를 실행 할 수 없다.
  • 스레드 A가 임계 영역내의 코드를 모두 실행하면 락을 반납한다. 이 때 다른 스레드들 중 하나가 락을 획득하여 임계영역내의 코드를 실행할 수 있다.
  • 특정 코드 구간을 임께 영역으로 설정할때 'synchronized'키워드를 사용한다. 두 가지 방법으로 사용한다.

 

    1. 메서드 전체를 임계 영역으로 지정하기

      메서드 반환 타입 좌측에 'synchronized'키워드 작성하면 메서드 전체를 임계영역으로 설정 할 수 있다.

class Account {
	...
	public synchronized boolean withdraw(int money) {
	    if (balance >= money) {
	        try { Thread.sleep(1000); } catch (Exception error) {}
	        balance -= money;
	        return true;
	    }
	    return false;
	}
}

 

    2. 특정한 영역을 임계 영역으로 지정하기

      'synchronized'키워드와 함께 소괄호()안에 해당 영역이 포함된 객체 참조를 넣고, 중괄호{}로 블럭을 열어 작성

class Account {
	...
	public boolean withdraw(int money) {
			synchronized (this) {
			    if (balance >= money) {
			        try { Thread.sleep(1000); } catch (Exception error) {}
			        balance -= money;
			        return true;
			    }
			    return false;
			}
	}
}

 

 


 

(Optional) 스레드의 상태와 실행 제어

 앞서 스레드 실행을 위해 'start()'메서드를 호출해야한다했지만 엄밀히말하면 'start()'는 실행 메서드가 아닌 스레드의 상태를 실행 대기 상태로 만들어주는 메서드이다. 어떤 메서드가 실행 대기 상태가 되면 운영체제가 적절한 때에 스레드를 실행한다.

  • 'start()'는 스레드를 실행 대기 상태로 만든다.
    • 스레드에는 상태라는 것이 있다.
    • 스레드의 상태를 바꿀수 있는 메서드가 있다.

 

 스레드의 상태와 실행 제어 메서드

1. sleep()

static void sleep(long milliSecond)
/

try { Thread.sleep(1000); } catch (Exception error) {}
  • sleep(long milliSecond) : milliSecond 동안 스레드를 잠시 멈춘다.
  • 'sleep()' : 스레드의 실행을 잠시 멈출때 사용한다.
  • 정지 시간을 설정할 수 있으나 항상 설정 시간만큼 중지되는 것은 아니며 약간의 오차가 있다.
  • 'sleep()'은 Thread의 클래스 메서드이다. 따라서 호출할떄 'Thread.sleep(1000);'과 같이 클래스를 통해 호출하는 것이 권장된다.
  • 'sleep()'를 호출하면 호출하는 코드를 실행한 스레드의 상태가 실행상태에서 일시정지 상태로 전환된다
  • 인자로 전달한 시간만큼의 시간이 경과한 경우, 'interrupt()' 호출한 경우 실행 대기 상태로 복귀한다.
  • 'sleep()'사용할때에는 try ... catch 문으로 감싸주어야 한다.

 

 

2. interrupt()

void interrupt()
  • interrupt() : 일시 중지 상태인 스레드를 실행 상태로 복귀시킨다.
  • 'sleep()', 'wait()', 'join()'에 의해 일시 정지 상태 스레드를 실행 대기 상태로 복귀시킨다.
  • 멈춰있는 스레드가 아닌 다른 스레드에서 '멈춰있는스레드.interrupt()'호출시 스레드를 멈추게했던 메서드에서 예외가 발생하여 일시정지가 풀리게 된다.
public class ThreadExample5 {
    public static void main(String[] args) {
        Thread thread1 = new Thread() {
            public void run() {
                try {
               		 // 정지상태에서 예외가 발생하면 catch문의 블록으로 이동
                    while (true) Thread.sleep(1000);
                }
                catch (Exception e) {} 
                // catch문의 블럭 실행 후 println() 실행
                System.out.println("Woke Up!!!");
            }
        };

        System.out.println("thread1.getState() = " + thread1.getState());
        
        thread1.start();

        System.out.println("thread1.getState() = " + thread1.getState());

        while (true) {
            if (thread1.getState() == Thread.State.TIMED_WAITING) {
                System.out.println("thread1.getState() = " + thread1.getState());
                break;
            }
        }

		//sleep중인 스레드에 예외를 발생 위의 catch문의 블록으로 이동한다.
        thread1.interrupt();

        while (true) {
            if (thread1.getState() == Thread.State.RUNNABLE) {
                System.out.println("thread1.getState() = " + thread1.getState());
                break;
            }
        }

        while (true) {
            if (thread1.getState() == Thread.State.TERMINATED) {
                System.out.println("thread1.getState() = " + thread1.getState());
                break;
            }
        }
    }
}

//출력 결과
thread1.getState() = NEW
thread1.getState() = RUNNABLE
thread1.getState() = TIMED_WAITING
Woke Up!!!
thread1.getState() = RUNNABLE
thread1.getState() = TERMINATED

Process finished with exit code 0

 

 

3. yield()

static void yield()
  • yield() : 다른 스레드에게 자신의 실행 시간을 양보한다.
  • 예를 들어 운영체제 스케줄러에 의해 3초를 할당받은 스레드 A가 1초 동안 작업을 수행하다가 'yield()' 호출하면 남은 실행 시간 2초는 다음 스레드에게 양보된다.
  • 특정 경우 반복문의 순회가 불필요할 때 사용한다.
public void run() {
		while (true) {
				if (example) {
						...
				}
				else Thread.yield();
		}
}

 

 

4. join()

void join()
void join(long milliSecond)
  • join() : 다른 스레드의 작업이 끝날 때까지 기다린다.
  • 특정 스레드가 작업하는 동안 자신을 일시 정지 상태로 만드는 상태 제어 메서드
  • 인자로 시간을 밀리초 단위로 전달 가능, 전달한 인자만큼의 시간이 경과하거나, 'interrupt()'호출되거나 'join()' 호출 시 지정했던 다른 스레드가 모든 작업을 마치면 다시 실행 대기 상태로 복귀한다.
  • 'join()'은 다음과 같은 측면에서 'sleep()'과 유사하다
    • join()을 호출한 스레드는 일시 중지 상태가 된다.
    • try ... catch문으로 감싸야 한다.
    • 'interrupt()'의해 실행 대기 상태로 복귀할 수 있다.
  • 차이점은 다음과 같다.
    • 'sleep'은 Thread 클래스의 static 메서드이다. 반면 join()은 특정 스레드에 대해 동작하는 인스턴스 메서드이다.
      • Thread.sleep(1000); // thread1.join();
public class ThreadExample {
    public static void main(String[] args) {
        SumThread sumThread = new SumThread();

        sumThread.setTo(10);

        sumThread.start();

        // 메인 스레드가 sumThread의 작업이 끝날 때까지 기다립니다. 
        try { sumThread.join(); } catch (Exception e) {}

        System.out.println(String.format("1부터 %d까지의 합 : %d", sumThread.getTo(), sumThread.getSum()));
    }
}

class SumThread extends Thread {
    private long sum;
    private int to;

    public long getSum() {
        return sum;
    }

    public int getTo() {
        return to;
    }

    public void setTo(int to) {
        this.to = to;
    }

    public void run() {
        for (int i = 1; i <= to; i++) {
            sum += i;
        }
    }
}

 

 

5. wait(), notify()

  • wait(), notify() : 스레드 간 협업에 사용된다. 두 스레드가 교대로 작업을 처리할때 사용 할 수 있다.
  • 스레드 A,B가 공유객체를 두고 협업하는 상황일 때, 스레드 A가 공유 객체에 자신의 작업을 완료하고,  스레드 B와 교대하기 위해 'notify()'호출한다. 호출되면 스레드 B가 실행 대기 상태가 되며, 곧 실행된다. 이어 A가 'wait()'을 호출하며 자기자신을 일시 정지 상태로 만든다.
public class ThreadExample5 {
    public static void main(String[] args) {
        WorkObject sharedObject = new WorkObject();

        ThreadA threadA = new ThreadA(sharedObject);
        ThreadB threadB = new ThreadB(sharedObject);

        threadA.start();
        threadB.start();
    }
}

class WorkObject {
    public synchronized void methodA() {
        System.out.println("ThreadA의 methodA Working");
        notify();
        try { wait(); } catch(Exception e) {}
    }

    public synchronized void methodB() {
        System.out.println("ThreadB의 methodB Working");
        notify();
        try { wait(); } catch(Exception e) {}
    }
}

class ThreadA extends Thread {
    private WorkObject workObject;

    public ThreadA(WorkObject workObject) {
        this.workObject = workObject;
    }

    public void run() {
        for(int i = 0; i < 10; i++) {
            workObject.methodA();
        }
    }
}

class ThreadB extends Thread {
    private WorkObject workObject;

    public ThreadB(WorkObject workObject) {
        this.workObject = workObject;
    }

    public void run() {
        for(int i = 0; i < 10; i++) {
            workObject.methodB();
        }
    }
}