✨ 프로그램이 실행되면 프로세스가 되고, 프로세스에서 여러 개의 스레드를 생성하여 작업을 수행한다.
📍 미리 알면 좋은 것
프로그램 : 프로그래밍 결과물로서 특정 기능을 하는 실행파일(.exe)
프로세스 : 실행 중인 프로그램 인스턴스, 프로그램을 실행하면 운영체제로부터 실행에 필요한 자원(메모리)를 할당받아 프로세스가 된다. 하나의 프로그램에서 여러 개의 프로세스가 생성될 수 있다.
멀티태스킹 : 여러 개의 프로세스가 동시에 실행되는 것. CPU의 코어가 한 번에 단 하나의 작업만 할 수 있기 때문에 실제로는 운영체제의 스케줄링에 따라 빠르게 번갈아가며 실행되어 사용자에게는 동시에 실행되는 것처럼 보이게 된다.
멀티스레딩 : 하나의 프로세스 내에서 여러 스레드가 동시에 작업을 수행하는 것, 여러 스레드가 같은 프로세스 내에서 자원을 공유한다.
+) 😊멀티스레딩의 장점 : CPU 사용률 향상, 자원을 효율적으로 사용, 사용자에 대한 응답성 향상, 작업의 분리로 코드 간결화 등
😢멀티스레딩의 단점 : 동기화, 교착상태 문제 방생의 가능성이 있음
✔ 멀티스레딩의 필요성
- 여러 작업을 동시에 수행해야 할 때 : 메신저로 채팅을 하면서 파일을 다운로드 받는 등.. 여러 작업을 동시에 수행할 수 있도록 해준다.
- 여러 사용자가 있는 서비스일 경우 : 하ㅏ의 서버 프로세스가 여러 개의 스레드를 생성하여 1사용자 = 1스레드로 요청이 처리되도록 해야 한다. 1사용자 = 1프로세스로 처리하면 매 요청마다 프로세스를 생성해야 한다. 프로세스를 생성하는 것은 스레드를 생성하는 것보다 더 많은 자원, 시간을 소모하기 때문에 부담이 크다. (*스레드 = 경량 프로세스)
아래의 2가지 방법 모두 run메소드를 스레드를 통해 작업하고자 오버라이딩하는 방식이다.
public class MyThread implements Runnable {
@Override
public void run() {
// 수행 코드
}
}
public class MyThread extends Thread {
@Override
public void run() {
// 수행 코드
}
}
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 인스턴스를 반환받아 사용함.
}
}
}
MyThread t1 = new MyThread1()
스레드의 호출을 start()로 해야 한다. (*run()이 아님)
✔ 그렇다면, 왜 굳이 start()로 스레드를 실행해야 할까?
run()으로 작업을 지시해도 일은 시작된다. 다만, run()메소드를 사용하면 스레드를 사용하지 않는다. 스레드를 이용한다는 것은 JVM이 다수의 콜 스택(call stack : 실질적인 명령어를 담고 있는 메모리)을 번갈아가며 일처리를 하여 사용자에게 동시에 작업하는 것처럼 보이는 것이다. 하지만 run()메소드를 호출하면 main()의 콜 스택 하나만을 이용하므로 스레드 활용이 아니다.
start()메소드 후출은 JVM이 알아서 스레드를 위한 콜스택을 새로 만들고 context switching으으로 번갈아가며 일을 처리한다.
스레드의 어려운 점은 동기화와 스케줄링이다.
✔ 스케줄링 관련된 메소드 : sleep(), join(), yield(), interrupt()
특히 join()은 start로 시작되고 main스레드가 모두 종료될 때까지 기다려주는 일을 해준다.
멀티스레드를 구현하면 동기화는 필수적이다. 여러 스레드가 같은 프로세스 내의 자원을 공유하면서 작업할 때 서로의 작업이 다른 작업에 영향을 주기 때문이다.
따라서, 임계영역(critical section)과 잠금(lock)을 활용한다.
임계영역을 지정하고, 임계영역을 가지고 있는 lock을 단 하나의 스레드에게만 빌려주는 개념으로 이루어져있다. 수행할 코드가 완료되면, lock을 반납해주어야 한다.
서로 다른 두 객체가 동기화를 하지 않은 메소드를 같이 오버라이딩해서 이용하면, 두 스레드가 동시에 진행되므로 문제가 발생할 수 있다.
이때 오버라이딩되는 부모 클래스의 메소드에 동기화(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("출금 완료");
}
스레드 간의 협력 작업을 강화하기 위해 사용한다. 동기화 처리한 메소드들이 반복문에서 활용되면 의도한 대로의 결과가 나오기 힘들다. 따라서 try-catch문 내에서 적절히 wait, notify를 사용하면 좋다.
wait() : 스레드가 lock을 가지고 있으면, lock 권한을 반납하고 대기하게 만듬
notify() : 대기 상태인 스레드에게 다시 lock 권한을 부여하고 수행하게 만듬