스레드

star_pooh·2024년 11월 18일
0

TIL

목록 보기
21/39
post-thumbnail

프로세스와 스레드

  • 프로세스 : 운영체제로부터 자원을 할당받는 작업의 단위 (실행 중인 프로그램)
  • 스레드 : 프로세스가 할당받은 자원을 이용하는 실행의 단위

프로세스 구조

  • 프로그램 Code + Data + 메모리 영역(Stack, Heap)
  • Code는 Java main 메소드와 같은 코드를 의미
  • Data는 프로그램이 실행 중 저장할 수 있는 저장 공간을 의미(전역변수, 정적변수 등 초기화된 데이터를 저장하는 공간)
  • Stack은 지역변수, 매개변수, 리턴 변수를 저장하는 공간
  • Heap은 프로그램이 동적으로 필요한 변수를 저장하는 공간

스레드

  • 스레드는 프로세스 내에서 일하는 일꾼(코드 실행의 흐름)
  • 프로세스가 작업 중인 프로그램에서 실행 요청이 들어오면 스레드를 만들어 명령을 처리
  • 프로세스 안에는 여러 스레드가 있고, 스레드는 실행을 위한 프로세스 내 주소공간과 메모리 공간(Heap)을 공유받음
  • 또한, 각각 명령 처리를 위한 자신만의 메모리 공간(Stack)도 할당받음

Java 스레드

  • Java 스레드도 일반 스레드와 동일하며, JVM 프로세스 안에서 실행되는 스레드를 의미
  • Java 스레드는 Java Main 스레드부터 실행되며 JVM에 의해 실행

멀티 스레드

싱글 스레드

  • 프로세스 안에서 하나의 스레드만 실행
  • Java 프로그램의 경우 main() 메소드만 실행시킨 것을 의미
  • main() 메소드의 스레드를 메인 스레드라고 칭함
  • JVM의 메인 스레드가 종료되면 JVM도 종료

멀티 스레드

  • 프로세스 안에서 여러 개의 스레드가 실행
  • 하나의 프로세스는 여러 개의 스레드를 가질 수 있으며, 스레드들은 프로세스의 자원을 공유
  • Java 프로그램은 멀티 스레드 생성 가능

  • 멀티 스레드 장점
    • 여러 개의 작업을 동시에 수행하여 성능이 향상됨
    • Stack을 제외한 모든 영역에서 메모리를 공유하기 때문에 효율적인 자원 사용 가능
    • 응답 스레드와 작업 스레드를 분리하여 빠른 응답 가능
  • 멀티 스레드 단점
    • 동기화 문제 발생 가능
      • 프로세스의 자원을 공유하기 때문에 자원을 서로 사용하려고 하는 충돌 발생
    • 교착 상태(Deadlock) 발생 가능
      • 둘 이상의 스레드가 서로의 자원을 원하는 상태가 되었을 때, 서로의 작업이 종료되기만을 기다리며 작업이 더 이상 진행되지 않는 상태

Thread와 Runnable

Java에서 스레드를 구현하고 실행하는 방법

Thread

public class TestThread extends Thread {
	@Override
	public void run() {
		// 스레드 수행작업
	}
}

TestThread thread = new TestThread(); // 스레드 생성
thread.start() // 스레드 실행

Runnable

public class TestRunnable implements Runnable {
	@Override
	public void run() {
		// 스레드 수행작업 
	}
}

Runnable run = new TestRunnable();
Thread thread = new Thread(run); // 스레드 생성
thread.start(); // 스레드 실행
// Runnable 인터페이스에 람다식을 사용하여 구현
public class Main {
    public static void main(String[] args) {
    	// 스레드 수행작업
        Runnable task = () -> {
            int sum = 0;
            for (int i = 0; i < 50; i++) {
                sum += i;
                System.out.println(sum);
            }
            // 현재 실행 중인 스레드의 이름 
           	System.out.println(Thread.currentThread().getName() + " 최종 합 : " + sum);
        };
		// 스레드에 이름 부여
        Thread thread1 = new Thread(task);
        thread1.setName("thread1");
        Thread thread2 = new Thread(task);
        thread2.setName("thread2");

        thread1.start();
        thread2.start();
    }
}
  • 확장성 및 자원 사용에서 유리하기 때문에 Thread보다 Runnable을 더 많이 사용
    • Runnable이 인터페이스이기 때문에 다중 상속 가능
    • 또한, 람다식 사용 가능
    • Thread는 Thread 클래스에 구현된 코드들에 의해 더 많은 자원을 사용

데몬 스레드와 사용자 스레드

데몬 스레드

  • 백그라운드(background)에서 실행되는 낮은 우선순위를 가진 스레드
  • 보조적인 역할을 담당하며 메모리 영역을 정리해주는 가비지 컬렉터(GC)가 대표적인 데몬 스레드
  • 다른 스레드가 모두 종료되면 작업이 남아있더라도 강제 종료 당함
public class Main {
    public static void main(String[] args) {
        Runnable demon = () -> {
            for (int i = 0; i < 1000000; i++) {
                System.out.println("demon");
            }
        };

        Thread thread = new Thread(demon);
        thread.setDaemon(true); // true로 설정시 데몬스레드로 실행
        thread.start();

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

사용자 스레드

  • 포그라운드(foreground)에서 실행되는 높은 우선순위를 가진 스레드
  • 프로그램 기능을 담당하며 메인 스레드가 대표적인 사용자 스레드

스레드 우선순위와 스레드 그룹

스레드 우선순위

  • 스레드 작업의 중요도에 따라 우선순위 부여 가능
  • 우선순위를 높게 지정하면 더 많은 작업시간을 부여받기 때문에 빠르게 처리될 수 있음
  • 스레드 생성 시 JVM 또는 사용자에 의해 지정
    • 최대 우선순위 (MAX_PRIORITY) = 10
    • 최소 우선순위 (MIN_PRIORITY) = 1
    • 보통 우선순위 (NROM_PRIORITY) = 5 (기본값)
    • OS가 아닌 JVM에서 설정한 우선순위
  • 우선순위가 높다고해서 반드시 먼저 종료되는 것은 아님
Thread thread1 = new Thread(task1);
// 스레드 우선순위 설정
thread1.setPriority(8);
// 스레드 우선순위 확인
int threadPriority = thread1.getPriority();
System.out.println("threadPriority = " + threadPriority);

스레드 그룹

  • 관련이 있는 스레드를 그룹으로 관리 가능
  • 모든 스레드는 반드시 하나의 그룹에 포함되어 있어야 함
  • 자신을 생성한 스레드(부모 스레드)의 그룹과 우선순위를 상속 받음
  • 기본적으로 system 그룹에 포함(JVM이 시작되면서 system 그룹 생성)
    • 메인 스레드는 system 그룹 하위의 main 그룹에 포함
    • 그룹을 지정하지 않을시 자동으로 main 그룹에 포함
public class Main {
    public static void main(String[] args) {
    	// 스레드 수행작업
        Runnable task = () -> {
        	while (!Thread.currentThread().isInterrupted()) {
            	try {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    break;
                }
            }
            System.out.println(Thread.currentThread().getName() + " Interrupted");
        };

        // ThreadGroup 클래스로 객체 생성
        ThreadGroup group1 = new ThreadGroup("Group1");
        // Thread 객체 생성시 첫 번째 매개변수에 설정
        // Thread(ThreadGroup group, Runnable target, String name)
        Thread thread1 = new Thread(group1, task, "Thread 1");
        Thread thread2 = new Thread(group1, task, "Thread 2");

        // 스레드 그룹 확인
        System.out.println("Group of thread1 : " + thread1.getThreadGroup().getName()); // Group of thread1 : Group1
        System.out.println("Group of thread2 : " + thread2.getThreadGroup().getName());  // Group of thread2 : Group1

        thread1.start();
        thread2.start();

        try {
            // 스레드를 지정된 시간동안 멈춤
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 일시정지 상태의 스레드를 실행 대기 상태로 변경
        group1.interrupt();
    }
}

스레드 상태와 제어

스레드 상태

상태Enum설명
객체 생성NEW스레드 객체 생성, start() 메소드 호출 전의 상태
실행 대기RUNNABLE스레드가 실행 가능한 상태
일시 정지WAITING다른 스레드가 통지(notify)할 때까지 기다리는 상태
TIMED_WAITING주어진 시간(Thread.sleep, ...)동안 기다리는 상태
BLOCKED사용하고자 하는 객체의 lock이 풀릴 때까지 기다리는 상태
종료TERMINATED스레드의 작업이 종료된 상태

스레드 제어


  • sleep
    • 스레드를 지정된 시간동안 일시 정지 상태로 변경
    • 특정 스레드를 멈추게 하는 것은 불가능
public class Main {
    public static void main(String[] args) {
        Runnable task = () -> {
            try {
            	// 밀리초 단위로 설정
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("task : " + Thread.currentThread().getName()); // task : Thread
        };

        Thread thread = new Thread(task, "Thread");
        thread.start();

        try {
        	// 특정 스레드를 멈추게 하는 것은 불가능
            thread.sleep(1000);
            System.out.println("sleep(1000) : " + Thread.currentThread().getName()); // sleep(1000) : main
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • interrupt
    • 일시 정지 상태인 스레드를 실행 대기 상태로 변경
    • isInterrupted() 메소드를 사용하여 상태 값 확인
    • sleep 상태에 있는 스레드가 interrupt를 만나면 재실행 되기 때문에 InterruptedException 발생
public class Main {
    public static void main(String[] args) {
        Runnable task = () -> {
            try {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("task : " + Thread.currentThread().getName());
        };

        Thread thread = new Thread(task, "Thread");
        thread.start();
        // sleep 상태인 스레드가 interrupt를 만나게 되므로 InterruptedException 발생
        thread.interrupt();
      	System.out.println("thread.isInterrupted() = " + thread.isInterrupted());
    }
}
  • join
    • 정해진 시간동안 지정한 스레드가 작업하는 것을 기다림
    • 시간을 지정하지 않으면 스레드의 작업이 끝날 때까지 기다림
    • interrupt를 만나면 기다리는 것을 멈추기 때문에 InterruptedException 발생
public class Main {
    public static void main(String[] args) {
        Runnable task = () -> {
            try {
                Thread.sleep(5000); // 5초
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        Thread thread = new Thread(task, "thread");
        thread.start();

        long start = System.currentTimeMillis();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // thread의 소요시간인 5000ms동안 main 스레드가 기다렸기 때문에 5000이상의 값이 출력
        System.out.println("소요시간 = " + (System.currentTimeMillis() - start));
    }
}
  • yield
    • 실행 중에 다른 스레드에게 실행을 양보하고 실행 대기 상태로 변경
public class Main {
    public static void main(String[] args) {
        Runnable task = () -> {
            try {
                for (int i = 0; i < 10; i++) {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName());
                }
            } catch (InterruptedException e) {
            	// InterruptedException이 발생한 스레드는 yield를 실행
                Thread.yield();
            }
        };

        Thread thread1 = new Thread(task, "thread1");
        Thread thread2 = new Thread(task, "thread2");
        thread1.start();
        thread2.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 5초 뒤 thread1에 interrupt 발생
        thread1.interrupt();
    }
}
  • synchronized
    • 어떤 스레드가 진행 중인 작업을 다른 스레드가 침범하지 못하도록 막는 것
    • 다른 스레드의 침범을 막아야 하는 코드들을 임계 영역으로 설정
    • 임계 영역에는 lock을 가진 단 하나의 스레드만 출입 가능
    • 메소드 전체 혹은 특정 부분을 임계 영역으로 지정
// 메소드 전체
public synchronized void asyncSum() {
	// 대상 코드
}
// 특정 영역
synchronized(해당 객체의 참조변수) {
	// 대상 코드
}
public class Main {
    public static void main(String[] args) {
        AppleStore appleStore = new AppleStore();

        Runnable task = () -> {
            while (appleStore.getStoredApple() > 0) {
                appleStore.eatApple();
                System.out.println("남은 사과의 수 = " + appleStore.getStoredApple()); 
            }
        };

        for (int i = 0; i < 3; i++) {
            new Thread(task).start();
        }
    }
}
// synchronized가 없을 때
class AppleStore {
    private int storedApple = 10;

    public int getStoredApple() {
        return storedApple;
    }

    public void eatApple() {
    	// 3개의 스레드가 사과를 공유해서 실행하기 때문에 존재하지 않는 사과를 먹는 경우가 생김
        // 10 9 8 ... 0 -1 -2
        if (storedApple > 0) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }    
            storedApple -= 1;
        }
    }
}

// synchronized가 있을 때
class AppleStore {
    private int storedApple = 10;

    public int getStoredApple() {
        return storedApple;
    }

    public void eatApple() {
    	// 한 스레드가 사과를 점유하면 나머지 두 스레드는 대기하기 때문에 존재하지 않는 사과를 먹는 경우는 발생하지 않음
        // 10 9 8 ... 0
        synchronized (this) {
            if(storedApple > 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                storedApple -= 1;
            }
        }
    }
}
  • wait
    • synchronized 블록 내에서 스레드를 일시 정지 상태로 변경
    • 객체의 대기실(waiting pool)에서 notify를 기다림
  • notify
    • synchronized 블록 내에서 wait에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 변경
    • 객체의 대기실(waiting pool)에 있는 모든 스레드 중 임의의 스레드만 notify를 받음
public class Main {
    public static String[] itemList = {
            "MacBook", "IPhone", "AirPods", "iMac", "Mac mini"
    };
    public static AppleStore appleStore = new AppleStore();
    public static final int MAX_ITEM = 5;

    public static void main(String[] args) {
        // 가게 점원
        Runnable StoreClerk = () -> {               
        	while (true) {
				int randomItem = (int) (Math.random() * MAX_ITEM);
                appleStore.restock(itemList[randomItem]);
                try {
                	Thread.sleep(50);
				} catch (InterruptedException ignored) {
				}
			}
		};
        // 고객
        Runnable Customer = () -> {
        	while (true) {
            	try {
                	Thread.sleep(77);
				} catch (InterruptedException ignored) {
				}
                int randomItem = (int) (Math.random() * MAX_ITEM);
                appleStore.sale(itemList[randomItem]);
                System.out.println(Thread.currentThread().getName() + " Purchase Item " + itemList[randomItem]);
			}
        };
        new Thread(StoreClerk, "StoreClerk").start();
        new Thread(Customer, "Customer1").start();
        new Thread(Customer, "Customer2").start();
        // 재고가 가득 있지만 고객이 원하는 제품이 없는 경우, 병목 현상 발생 (점원은 재고가 하나라도 비어야 재입고를 하기 때문에)
    }
}

class AppleStore {
    private List<String> inventory = new ArrayList<>();

    public void restock(String item) {
        synchronized (this) {
            while (inventory.size() >= Main.MAX_ITEM) {
                System.out.println(Thread.currentThread().getName() + " Waiting!");
                try {
                    wait(); // 재고가 가득 있는 경우에는 재입고 하지 않고 대기
                    Thread.sleep(333);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 재입고
            inventory.add(item);
            notify(); // 고객에게 재입고 완료를 알림
            System.out.println("Inventory 현황: " + inventory.toString());
        }
    }

    public synchronized void sale(String itemName) {
        while (inventory.size() == 0) {
            System.out.println(Thread.currentThread().getName() + " Waiting!");
            try {
                wait(); // 재고가 없어서 구매를 할 수 없기에 대기
                Thread.sleep(333);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        while (true) {
            for (int i = 0; i < inventory.size(); i++) {
            	// 고객이 찾는 제품이 있을 경우
                if (itemName.equals(inventory.get(i))) {
                    inventory.remove(itemName);
                    notify(); // 제품이 팔려서 재고가 비었으니 재입고가 필요한 것을 알림
                    return;
                }
            }

            try {
                System.out.println(Thread.currentThread().getName() + " Waiting!");
                wait(); // 고객이 찾는 제품이 없을 경우 대기
                Thread.sleep(333);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  • Lock
    • 같은 메소드 내에서만 Lock을 걸 수 있다는 synchronized의 제약을 해소하기 위해 Lock 클래스 사용
    • ReentrantLock
      • 재진입이 가능하며 가장 일반적인 배타 Lock
      • 같은 스레드가 이미 Lock을 가지고 있더라도 Lock을 유지하며 계속 실행 가능하기 때문에 deadlock이 발생하지 않음
      • 코드의 유연성을 높임
    • ReentrantReadWriteLock
      • 읽기 Lock과 쓰기 Lock을 별도로 제공
      • 읽기에는 공유적이고 쓰기에는 배타적
      • 읽기 Lock이 걸려 있으면 다른 스레드들도 읽기 Lock을 중복으로 걸고 읽기 수행 가능 (read-only)
      • 읽기 Lock이 걸려 있는 상태에서 쓰기 Lock을 거는 것은 허용 되지 않음 (데이터 변경 방지)
    • StampedLock
      • ReentrantReadWriteLock에 낙관적인 Lock의 기능 추가
      • 읽기와 쓰기 작업 모두 빠름
      • 낙관적인 읽기 Lock은 쓰기 Lock에 의해 해제 가능
      • 읽기 Lock을 무조건 걸지 않고, 쓰기와 읽기가 충돌할 때만 쓰기 후 읽기 Lock을 걸게됨

💡낙관적인 Lock이란?
데이터를 변경하지 전에 Lock을 걸지 않는 것(즉, 데이터를 변경할 때만 Lock을 거는 것)
그렇게 때문에 데이터 변경을 할 때 충돌이 일어날 가능성이 적은 상황에서 사용

  • Condition
    • waiting pool 내의 스레드를 구분하지 못하는 것을 해결 (wait와 notify의 문제점)
    • awaitsignal 사용
public class Main {
    public static final int MAX_TASK = 5;

    private ReentrantLock lock = new ReentrantLock();
    // lock으로 condition 생성
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();

    private ArrayList<String> tasks = new ArrayList<>();

    // 작업 메소드
    public void addMethod(String task) {
		lock.lock(); // 임계 영역 시작

    	try {
    		while(tasks.size() >= MAX_TASK) {
        		String name = Thread.currentThread().getName();
            	System.out.println(name+" is waiting.");
            	try {
	            	condition1.await(); // condition1 스레드를 일시 정지 상태로 변경
	                Thread.sleep(500);
				} catch(InterruptedException e) {}	
			}
			tasks.add(task);
        	condition2.signal(); // condition2 스레드를 실행 대기 상태로 변경
        	System.out.println("Tasks:" + tasks.toString());
		} finally {
    		lock.unlock(); // 임계 영역 종료
		}
	}
}

0개의 댓글

관련 채용 정보