[CS-JAVA] 스레드

지영·2023년 8월 21일
0

CS

목록 보기
59/77

프로그램이 실행되면 프로세스가 되고, 프로세스에서 여러 개의 스레드를 생성하여 작업을 수행한다.

📍 미리 알면 좋은 것

프로그램 : 프로그래밍 결과물로서 특정 기능을 하는 실행파일(.exe)
프로세스 : 실행 중인 프로그램 인스턴스, 프로그램을 실행하면 운영체제로부터 실행에 필요한 자원(메모리)를 할당받아 프로세스가 된다. 하나의 프로그램에서 여러 개의 프로세스가 생성될 수 있다.
멀티태스킹 : 여러 개의 프로세스가 동시에 실행되는 것. CPU의 코어가 한 번에 단 하나의 작업만 할 수 있기 때문에 실제로는 운영체제의 스케줄링에 따라 빠르게 번갈아가며 실행되어 사용자에게는 동시에 실행되는 것처럼 보이게 된다.
멀티스레딩 : 하나의 프로세스 내에서 여러 스레드가 동시에 작업을 수행하는 것, 여러 스레드가 같은 프로세스 내에서 자원을 공유한다.
+) 😊멀티스레딩의 장점 : CPU 사용률 향상, 자원을 효율적으로 사용, 사용자에 대한 응답성 향상, 작업의 분리로 코드 간결화 등
😢멀티스레딩의 단점 : 동기화, 교착상태 문제 방생의 가능성이 있음

✔ 멀티스레딩의 필요성

  • 여러 작업을 동시에 수행해야 할 때 : 메신저로 채팅을 하면서 파일을 다운로드 받는 등.. 여러 작업을 동시에 수행할 수 있도록 해준다.
  • 여러 사용자가 있는 서비스일 경우 : 하ㅏ의 서버 프로세스가 여러 개의 스레드를 생성하여 1사용자 = 1스레드로 요청이 처리되도록 해야 한다. 1사용자 = 1프로세스로 처리하면 매 요청마다 프로세스를 생성해야 한다. 프로세스를 생성하는 것은 스레드를 생성하는 것보다 더 많은 자원, 시간을 소모하기 때문에 부담이 크다. (*스레드 = 경량 프로세스)

1. 스레드 구현

아래의 2가지 방법 모두 run메소드를 스레드를 통해 작업하고자 오버라이딩하는 방식이다.

1-1. Runnable 인터페이스 구현(implements)

public class MyThread implements Runnable {
    @Override
    public void run() {
        // 수행 코드
    }
}

1-2. Thread 클래스 상속(extends)

public class MyThread extends Thread {
    @Override
    public void run() {
        // 수행 코드
    }
}

2. 스레드 구현 후 생성

2-1. Runnable 인터페이스를 구현하여 생성하기

Runnable 인터페이스를 구현하는 MyThread클래스의 인스턴스를 생성하고,Thread 생성자의 매개변수로 넘김.

Runnable r = new MyThread();
Thread t1 = new Thread(r);

public class ThreadEx1 {
	public static void main(String[] args) {
    	MyThread1 t1 = new MyThread1(); // Thread 상속 클래스 인스턴스
        
        Runnable r = new MyThread2(); // Runnable 구현 클래스 인스턴스
        Thread t2 = new Thread(r); // Thread 생성자
        
        t1.start();
        t2.start();
     }
 }
 
 // Thread 클래스를 상속받는 방법 
 public class MyThread1 extends Thread{
 	@Override 
    public void run(){
    	for(int i=0;i<5;i++){
        	System.out.println(getName()); // 부모 클래스의 getName() 메서드 호출
            }
        }
  }
  
 
  // Runnable 인터페이스를 구현하는 방법 
 public class MyThread2 extends Thread{
 	@Override 
    public void run(){
    	for(int i=0;i<5;i++){
        	System.out.println(Thread.currentThread().getName()); // Thread클래스를 상속받지 않아 getName()메소드가 없기 때문에  Thread클래스의 static메서드, currentThread()를 호출하여 Thread 인스턴스를 반환받아 사용함. 
            }
        }
  }
  • String getName() : 스레드의 이름을 반환
  • static Thread currentThread() : 현재 실행중인 스레드으 참조를 반환

2-2. Thread 클래스를 상속받아 생성하기

MyThread t1 = new MyThread1() 

3. 스레드 실행

start()는 스레드가 작업을 실행하는데 필요한 콜 스택을 생성한 다음 run()을 호출해서 그 스택 안에 run()을 저장할 수 있도록 해준다.

스레드의 호출을 start()로 해야 한다. (*run()이 아님)

✔ 그렇다면, 왜 굳이 start()로 스레드를 실행해야 할까?
run()으로 작업을 지시해도 일은 시작된다. 다만, run()메소드를 사용하면 스레드를 사용하지 않는다. 스레드를 이용한다는 것은 JVM이 다수의 콜 스택(call stack : 실질적인 명령어를 담고 있는 메모리)을 번갈아가며 일처리를 하여 사용자에게 동시에 작업하는 것처럼 보이는 것이다. 하지만 run()메소드를 호출하면 main()의 콜 스택 하나만을 이용하므로 스레드 활용이 아니다.
start()메소드 후출은 JVM이 알아서 스레드를 위한 콜스택을 새로 만들고 context switching으으로 번갈아가며 일을 처리한다.

4. 스레드의 실행제어

스레드의 어려운 점은 동기화스케줄링이다.

✔ 스케줄링 관련된 메소드 : sleep(), join(), yield(), interrupt()
특히 join()은 start로 시작되고 main스레드가 모두 종료될 때까지 기다려주는 일을 해준다.

4-1. 스레드 상태

  • NEW : 스레드가 생성되고 아직 start()가 호출되지 않은 상태
  • RUNNABLE : 실행 중 또는 실행 가능한 상태
  • BLOCKED : 동기화 블록에 의해 일시정지된 상태 (락이 풀릴 때까지 기다림)
  • WAITING, TIME_WAITING : 실행가능하지 않은 일시정지 상태
  • TERMINATED : 스레드 작업이 종료된 상태

4-2. 동기화

동기화의 필요성

멀티스레드를 구현하면 동기화는 필수적이다. 여러 스레드가 같은 프로세스 내의 자원을 공유하면서 작업할 때 서로의 작업이 다른 작업에 영향을 주기 때문이다.

따라서, 임계영역(critical section)잠금(lock)을 활용한다.

임계영역을 지정하고, 임계영역을 가지고 있는 lock을 단 하나의 스레드에게만 빌려주는 개념으로 이루어져있다. 수행할 코드가 완료되면, lock을 반납해주어야 한다.

동기화 방법

  • 임계 영역(critical section) : 공유 자원에 단 하나으 스레드만 접근하도록(하나의 프로세스에 속한 스레드만 가능)
  • 뮤텍스(mutex) : 공유 자원에 단 하나의 스레드만 접근하도록 (서로 다른 프로세스에 속한 스레드도 가능)
  • 이벤드(event) : 특정한 사건 발생을 다른 스레드에게 알림
  • 세마포어(semaphore) : 한정된 개수의 자원을 여러 스레드가 사용하려고 할 때 접근을 제한
  • 대기 가능 타이머(waitable timer) : 특정 시간이 되면 대기 중이던 스레드를 깨움

동기화 활용

서로 다른 두 객체가 동기화를 하지 않은 메소드를 같이 오버라이딩해서 이용하면, 두 스레드가 동시에 진행되므로 문제가 발생할 수 있다.

이때 오버라이딩되는 부모 클래스의 메소드에 동기화(synchronized) 키워드로 임계영역을 설정해주면 해결할 수 있다.

//synchronized : 스레드의 동기화. 공유 자원에 lock
public synchronized void saveMoney(int save){    // 입금
    int m = money;
    try{
        Thread.sleep(2000);    // 지연시간 2초
    } catch (Exception e){

    }
    money = m + save;
    System.out.println("입금 처리");

}

public synchronized void minusMoney(int minus){    // 출금
    int m = money;
    try{
        Thread.sleep(3000);    // 지연시간 3초
    } catch (Exception e){

    }
    money = m - minus;
    System.out.println("출금 완료");
}

wait()와 notify()의 활용

스레드 간의 협력 작업을 강화하기 위해 사용한다. 동기화 처리한 메소드들이 반복문에서 활용되면 의도한 대로의 결과가 나오기 힘들다. 따라서 try-catch문 내에서 적절히 wait, notify를 사용하면 좋다.

  • wait() : 스레드가 lock을 가지고 있으면, lock 권한을 반납하고 대기하게 만듬

  • notify() : 대기 상태인 스레드에게 다시 lock 권한을 부여하고 수행하게 만듬






*참고 : https://bada744.tistory.com/124
profile
꾸준함의 힘을 아는 개발자📍

0개의 댓글