- 프로그램 Code + Data + 메모리 영역(Stack, Heap)
- Code는 Java main 메소드와 같은 코드를 의미
- Data는 프로그램이 실행 중 저장할 수 있는 저장 공간을 의미(전역변수, 정적변수 등 초기화된 데이터를 저장하는 공간)
- Stack은 지역변수, 매개변수, 리턴 변수를 저장하는 공간
- Heap은 프로그램이 동적으로 필요한 변수를 저장하는 공간
- 스레드는 프로세스 내에서 일하는 일꾼(코드 실행의 흐름)
- 프로세스가 작업 중인 프로그램에서 실행 요청이 들어오면 스레드를 만들어 명령을 처리
- 프로세스 안에는 여러 스레드가 있고, 스레드는 실행을 위한 프로세스 내 주소공간과 메모리 공간(Heap)을 공유받음
- 또한, 각각 명령 처리를 위한 자신만의 메모리 공간(Stack)도 할당받음
- Java 스레드도 일반 스레드와 동일하며, JVM 프로세스 안에서 실행되는 스레드를 의미
- Java 스레드는 Java Main 스레드부터 실행되며 JVM에 의해 실행
- 프로세스 안에서 하나의 스레드만 실행
- Java 프로그램의 경우
main()
메소드만 실행시킨 것을 의미main()
메소드의 스레드를메인 스레드
라고 칭함- JVM의 메인 스레드가 종료되면 JVM도 종료
멀티 스레드
- 프로세스 안에서 여러 개의 스레드가 실행
- 하나의 프로세스는 여러 개의 스레드를 가질 수 있으며, 스레드들은 프로세스의 자원을 공유
- Java 프로그램은 멀티 스레드 생성 가능
- 멀티 스레드 장점
- 여러 개의 작업을 동시에 수행하여 성능이 향상됨
- Stack을 제외한 모든 영역에서 메모리를 공유하기 때문에 효율적인 자원 사용 가능
- 응답 스레드와 작업 스레드를 분리하여 빠른 응답 가능
- 멀티 스레드 단점
- 동기화 문제 발생 가능
- 프로세스의 자원을 공유하기 때문에 자원을 서로 사용하려고 하는 충돌 발생
- 교착 상태(Deadlock) 발생 가능
- 둘 이상의 스레드가 서로의 자원을 원하는 상태가 되었을 때, 서로의 작업이 종료되기만을 기다리며 작업이 더 이상 진행되지 않는 상태
Java에서 스레드를 구현하고 실행하는 방법
public class TestThread extends Thread { @Override public void run() { // 스레드 수행작업 } } TestThread thread = new TestThread(); // 스레드 생성 thread.start() // 스레드 실행
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의 문제점)
await
와signal
사용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(); // 임계 영역 종료 } } }