본문 바로가기

항해 99/Java

Java - 프로세스, 쓰레드

프로세스(Process) , 쓰레드(Thread)

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

프로세스는 "실행 중인 프로그램"을 의미

 

프로세스 구조

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

쓰레드는 프로세스 내에서 일하는 일꾼

 

쓰레드 생성

  • 프로세스가 작업 중인 프로그램에서 실행요청이 들어오면 쓰레드를 만들어 명령을 처리

쓰레드의 자원

  • 프로세스 안에는 여러 쓰레드가 있고, 쓰레드들은 실행을 위한 프로세스 내 주소공간이나 메모리공간(Heap)를 공유 받음
  • 쓰레드들은 각각 명령처리를 위한 자신만의 메모리공간(Stack)도 할당받음

Java 쓰레드

  • Java 프로그램 실행 시 JVM 프로세스 위에서 실행
  • Java 프로그램 쓰레드는 Java Main 쓰레드부터 실행, JVM에 의해 실행.

 

멀티 쓰레드

Java는 메인 쓰레드가 main()메서드를 실행시키면서 시작(자바는 멀티 쓰레드 지원)

  • 싱글 쓰레드
    • main( ) 메서드만 실행시켰을 때를 싱글 쓰레드라 함
    • Java 프로그램 main( ) 메서드의 쓰레드는 '메인 쓰레드'
    • JVM 메인 쓰레드 종료 시 JVM도 종료
  • 멀티 쓰레드
    • 프로세스 안에서 여러 개의 쓰레드가 실행되는 것
    • 하나의 프로세스는 여러 개의 쓰레드를 가질 수 있으며, 쓰레드들은 프로세스의 자원을 공유
    • Java 프로그램은 메인 쓰레드 외의 다른 작업 쓰레드를 생성하여 여러 개의 실행흐름을 만들 수 있음
    • 장점
      • 여러 개의 쓰레드를 통해 여러 개의 작업을 동시에 할 수 있어 성능 향상
      • 스택을 제외한 모든 영역에서 메모리를 공유해 자원을 보다 효율적으로 사용
      • 응답 쓰레드와 작업 쓰레드를 분리하여 빠르게 응답을 줄 수 있음(비동기)
    • 단점
      • 동기화 문제 발생 가능(프로세스 자원을 공유하며 작업을 처리해 자원을 서로 사용하려고 충돌이 발생)
      • 교착 상태(데드락)이 발생 가능(둘 이상의 쓰레드가 서로의 자원을 원하는 상태가 되어 서로 작업이 종료되기만 기다리며 작업을 더 이상 진행하지 못하는 상태)

 

Thread , Runnable

 

Thread - 자바에서 사용하는 Thread를 상속 받아 구현

package week05.thread;

// 1. Thread Class 이용(상속)
public class TestThread extends Thread{
    @Override
    public void run() {
        // 실제 쓰레드에서 수행할 작업
        for (int i = 0; i < 100; i++) {
            System.out.print("*");
        }
    }
}

 

Runnable - 자바에서 제공하는 Runnable 인터페이스 사용하여 구현

package week05.thread;

public class TestRunnable implements Runnable{
    @Override
    public void run() {
        // 쓰레드에서 수행할 작업 정의
        for (int i = 0; i < 100; i++) {
            System.out.print("$");
        }
    }
}
  • Runnable은 인터페이스이기 때문에 다른 필요한 클래스를 상속 받을 수 있음(확장성에 매우 유리함)

Main

package week05.thread;

public class Main {
    public static void main(String[] args) {
//        TestThread thread = new TestThread();
//        thread.start();
        Runnable run = new TestRunnable();
        Thread thread = new Thread(run);

        thread.start();
    }
}

 

Main - lambda

package week05.thread;

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();
    }
}

 

 

 

싱글 쓰레드 , 멀티 쓰레드 사용 예제

싱글 쓰레드

package week05.thread.single;

public class Main {
    public static void main(String[] args) {
        Runnable task = () -> {
            System.out.println("2번 => " + Thread.currentThread().getName());
            for (int i = 0; i < 100; i++) {
                System.out.print("$");
            }
        };

        System.out.println("1번 => " + Thread.currentThread().getName());
        Thread thread1 = new Thread(task);
        thread1.setName("thread1");

        thread1.start();
    }
}

 

멀티 쓰레드

package week05.thread.multi;

public class Main {
    public static void main(String[] args) {
        // 걸리는 시간이나, 동작을 예측할 수 없다

        // 1st
        Runnable task = () -> {
            for (int i = 0; i < 100; i++) {
                System.out.print("$");
            }
        };
        // 2nd
        Runnable task2 = () -> {
            for (int i = 0; i < 100; i++) {
                System.out.print("*");
            }
        };


        Thread thread1 = new Thread(task);
        thread1.setName("thread1");
        Thread thread2 = new Thread(task2);
        thread2.setName("thread2");

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

 

 

 

데몬 쓰레드

background에서 실행되는 낮은 우선순위를 가진 쓰레드, 보조적인 역할을 담당(대표적인 데몬 쓰레드는 GC가 있음).

 

데몬 쓰레드 예제

package week05.thread.demon;

public class Main {
    public static void main(String[] args) {
        Runnable demon = () -> {
            for (int i = 0; i < 1000000; i++) {
                System.out.println(i +" 번째 demon");
            }
        };

        // 우선순위가 낮다 -> 상대적으로 다른 쓰레드에 비해 리소스를 적게 할당 받음
        Thread thread = new Thread(demon);
        thread.setDaemon(true);

        thread.start();

        for (int i = 0; i < 100; i++) {
            System.out.println(i + " 번째 task");
        }
    }
}
  • JVM은 사용자 쓰레드의 작업이 끝나면 데몬 쓰레드도 자동으로 종료시킴

 

사용자 쓰레드

  • 보이는 곳(foreground)에서 실행되는 높은 우선순위를 가진 쓰레드
  • 프로그램 기능을 담당, 대표적인 쓰레드로는 메인 쓰레드가 있음
  • 사용자 쓰레드 만드는 법 : 기존에 만들었던 쓰레드가 다 사용자 쓰레드

 

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

쓰레드 우선순위

  • 작업 중요도에 따라 쓰레드의 우선순위를 부여할 수 있음(우선순위가 높게 지정되면 더 많은 작업시간을 부여받아 빠르게 처리될 수 있음)

우선순위는 3가지로 나뉨(JVM에서 설정한 우선 순위)

  • 최대 우선순위 (MAX_PRIORITY) = 10
  • 최소 우선순위 (MIN_PRIORITY) = 1
  • 보통 우선순위 (NROM_PRIORITY) = 5
    • 기본 값 : 보통 우선순위
    • 자세히 나눌 때 1~10 사이의 숫자로 지정 가능

쓰레드 우선 순위는 setPriority( ) 로 설정

쓰레드 우선 순위 예제

package week05.thread.priority;

public class Main {
    public static void main(String[] args) {
        Runnable task1 = () -> {
            for (int i = 0; i < 100; i++) {
                System.out.print("$");
            }
        };

        Runnable task2 = () -> {
            for (int i = 0; i < 100; i++) {
                System.out.print("*");
            }
        };

        Thread thread1 = new Thread(task1);
        thread1.setPriority(8);
        int threadPriority = thread1.getPriority();
        System.out.println("threadPriority = " + threadPriority);

        Thread thread2 = new Thread(task2);
        thread2.setPriority(2);

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

 

 

쓰레드 그룹

  • 서로 관련이 있는 쓰레들을 그룹으로 묶어서 다루는 것
    • JVM 시작 시 system 그룹이 생성되고 쓰레드들은 기본적으로 system 그룹에 포함
  • 메인 쓰레드는 system 그룹 하위에 있는 main 그룹에 포함
  • 모든 쓰레드들은 반드시 하나의 그룹에 포함되어 있어야 함
    • 쓰레드 그룹을 지정받지 못한 쓰레드는 자신을 생성한 부모 쓰레드의 그룹과 우선순위를 상속 받음
    • 쓰레드 그룹 미지정 시 자동으로 main 그룹에 포함 됨

쓰레드 그룹 생성(ThreadGroup 클래스로 객체를 만들고 Thread 객체 생성 시 첫 번째 매개변수로 넣어주면 됨)

 

쓰레드 그룹 예제

package week05.thread.group;

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");

        // Thread에 ThreadGroup이 할당된 것을 확인
        System.out.println("Group of thread1 : " + thread1.getThreadGroup().getName());
        System.out.println("Group of thread2 : " + thread2.getThreadGroup().getName());

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

        try {
            // 현재 쓰레드를 지정된 시간동안 멈추게 함
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // interrupt()는 일시정지 상태인 쓰레드를 실행대기 상태로 만든다
        group1.interrupt();
    }
}

 

 

 

쓰레드 상태와 제어

쓰레드 상태

  • 쓰레드는 실행과 대기를 반복하며 run() 메서드를 수행, 메서드 종료 시 실행이 멈춤

  • 일시정지 상태에서는 쓰레드가 실행을 할 수 없는 상태가 됨
  • 쓰레드가 다시 실행 상태로 넘아가기 위해서는 일시정지 상태에서 실행대기 상태로 넘어가야 함

쓰레드 상태 정리 표

 

 

 

쓰레드 제어

 

sleep()

  • 현재 쓰레드를 지정된 시간동안 멈추게 한다
    • sleep()은 쓰레드가 자기 자신에 대해서만 멈추게 할 수 있음

예제

package week05.thread.stat.sleep;

public class Main {
    public static void main(String[] args) {
        Runnable task = () -> {
            try {
                // (1) 예외 처리 필수
                // - interrupt()를 만나면 다시 실행되기 때문에
                // - InterruptException 발생 가능
                // (2) 특정 쓰레드 지목 불가
                Thread.sleep(2000); // TIMED_WAITING(주어진 시간동안만 기다리는 상태)
                // 객체.메서드();
                
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("task : " + Thread.currentThread().getName());
        };

        Thread thread = new Thread(task, "Thread"); // NEW
        thread.start(); // NEW -> RUNNABLE

        try {
            // 1초가 지나고 나면 runnable 상태로 변하여 다시 실행
            // 특정 쓰레드를 지목해서 멈추게 하는 것은 불가능
            // Static member 'java.lang.Thread.sleep(long)' accessed via instance reference
            thread.sleep(1000);
            System.out.println("sleep(1000) : " + Thread.currentThread().getName());

        } catch (InterruptedException e) {
            
            e.printStackTrace();
        }
    }
}

 

 

interrupt()

  • 일시정지 상태인 쓰레드를 실행대기 상태로 만든다
    • isInterrupted() 메서드를 사용하면 쓰레드의 상태값을 확인할 수 있음

예제 코드

package week05.thread.stat.interrupt;

// - 쓰레드가 'start()' 된 후 동작하다 'interrupt()'를 만나 실행하면 interrupted 상태가 true가 됨

//public class Main {
//    public static void main(String[] args) {
//        Runnable task = () -> {
//            try {
//                // sleep 도중 interrupt 발생 시, catch
//                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"); // NEW
//        thread.start(); // NEW -> Runnable
//
//        thread.interrupt();
//
//        System.out.println("thread.isInterrupted() = " + thread.isInterrupted());
//    }
//}


public class Main {
    public static void main(String[] args) {
        Runnable task = () -> {
            // interrupted 상태를 체크해서 처리하면 오류를 방지
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    break;
                }
            }
            System.out.println("task : " + Thread.currentThread().getName());
        };

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

        thread.interrupt();

        System.out.println("thread.isInterrupted() = " + thread.isInterrupted());
    }
}

 

 

 

join, yield, sychronized

  • join()
    • 정해진 시간동안 지정한 쓰레드가 작업하는 것을 기다림(시간을 지정 안 하면 지정한 쓰레드의 작업이 끝날 때까지 기다림)
  • 사용법
Thread thread = new Thread(task, "thread");

thread.start();

try {
    thread.join();
} catch (InterruptedException e) {
    e.printStackTrace();
}

 

예제

package week05.thread.stat.join;

import org.w3c.dom.ls.LSOutput;

// 정해진 시간동안 지정한 쓰레드가 작업하는 것을 기다림
// - 시간 지정 없으면 지정한 쓰레드의 작업이 끝날 때까지 기다림
public class Main {
    public static void main(String[] args) {
        Runnable task = () -> {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        Thread thread = new Thread(task, "thread"); // NEW

        thread.start(); // NEW -> Runnable

        long start = System.currentTimeMillis();

        try {
            // 시간 지정 없으므로 thread가 작업을 끝낼 때까지 main 쓰레드는 기다리게 됨
            thread.join();

        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // thread의 소요시간인 5000ms 동안 main 쓰레드가 기다렸기 때문에 5000 이상이 출력
        System.out.println("소요시간 = " + (System.currentTimeMillis() - start));
    }
}

 

  • yield()
    • 남은 시간을 다음 쓰레드에게 양보하고 쓰레드 자신은 실행대기 상태가 됨
  • 사용법
package week05.thread.stat.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) {
                    Thread.yield();
//                    e.printStackTrace();
            }
        };

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

        thread1.start(); // NEW -> RUNNABLE
        thread2.start(); // NEW -> RUNNABLE

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        thread1.interrupt();
    }
}

 

 

  • synchronized(쓰레드 동기화)
    • 한 쓰레드가 진행중인 작업을 다른 쓰레드가 침범하지 못하도록 막는 것
    • 동기화를 위해서는 다른 쓰레드의 침범을 막아야 하는 코드들을 '임계영역'으로 설정
      • 임계영역은 Lock을 가진 단 하나의 쓰레드만 출입이 가능
  • 임계영역 지정
    • 실행할 메서드 또는 실행할 코드 묶음 앞에 sychronized를 붙여서 다른 쓰레드의 침범을 막을 수 있음
      • 메서드 전체를 임계영역으로 지정
      • 특정 영역을 임계영역으로 지정

예제 코드

package week05.thread.stat.sync;

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();
        }
    }
}

class AppleStore {
    private int storedApple = 10;

    public int getStoredApple() {
        return storedApple;
    }

    public void eatApple() {
        synchronized (this) {
            if(storedApple > 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                storedApple -= 1;
            }
        }
    }
}

 

 

wait, notify

  • wait() 와 notify는 쌍(같이 쓰임)
    • wait( ) : 실행 중이던 쓰레드는 해당 객체의 대기실(wating pool)에서 통지를 기다림
    • notify( ) : 해당 객체의 대기실(waiting pool)에 있는 모든 쓰레드 중에서 임의의 쓰레드만 통지를 받는다

예제 코드

package week05.thread.stat.waitnotify;

import java.util.ArrayList;
import java.util.List;

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 StroeClerk = () -> {
            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(StroeClerk, "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());
                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 1!");
            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 2!");
                wait();
                Thread.sleep(333);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

  • wait(), notify() 사용 시에는 병목현상이 발생하지 않도록 주의

 

 

Lock, Condition

  • Lock
    • synchronized 블럭으로 동기화하면 자동적으로 Lock이 걸리고 풀리지만, 같은 메서드 내에서만 Lock을 걸 수 있다는 제약을 해결하기 위해 사용
    • ReentrantLock
      • 재진입 가능한 Lock, 가장 일반적인 배타 Lock
      • 특정 조건에서 Lock을 풀고, 나중에 다시 Lock을 얻어 임계영역으로 진입 가능
      • 같은 쓰레드가 이미 락을 가지고 있더라도 락을 유지하며 실행할 수 있어 데드락이 발생하지 않음(코드의 유연성을 높일 수 있음)
    • ReentrantReadWriteLock
      • 읽기를 위한 Lock과 쓰기를 위한 Lock을 따로 제공
      • 읽기에는 공유적이고, 쓰기에는 베타적인 Lock
      • 읽기 Lock이 걸리면 다른 쓰레드들도 읽기 Lock을 중복으로 걸고 읽기를 수행 가능(read-only)
      • 읽기 Lock이 걸린 상태에서 쓰기 Lock을 거는 것은 불가(데이터 변경 방지)
    • StampedLock
      • ReentrantReadWriteLock에 낙관적인 Lock의 기능을 추가
        • 낙관적인 Lock : 데이터 변경 전에 락을 걸지 않음(데이터 변경을 할 때 충돌 가능성이 낮은 경우 사용)
        • 읽기와 쓰기 작업 모두 빠르게 처리. 쓰기 작업이 발생 시 데이터가 변경된 경우 다시 읽기 작업을 수행해 새로운 값을 읽고 변경 작업을 다시 수행(쓰기 작업이 빈번하지 않은 경우 더 빠른 처리 가능)
      • 낙관적인 읽기 Lock은 쓰기 Lock에 의해 바로 해제 가능
      • 무조건 읽기 Lock을 걸지 않고, 쓰기와 읽기가 충돌할 때만 쓰기 후 읽기 Lock 검
  • Condition
    • wait()와 notify()의 문제점인 waiting pool 내 쓰레드를 구분하지 못한다는 것을 해결(waiting pool 내의 쓰레드를 분리하여 특정 조건을 만족할 때만 쓰레드를 깨움)
    • Codintion의 await() & signal()을 사용

예제 코드

package week05.thread.stat.condition;

import java.util.ArrayList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

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(); // wait(); condition1 쓰레드를 기다리게 함
                    Thread.sleep(500);
                } catch (InterruptedException e) {}
            }

            tasks.add(task);
            condition2.signal(); // notify(); 기다리고 있는 condition2를 깨워줌
            System.out.println("Tasks:" + tasks.toString());
        } finally {
            lock.unlock(); // 임계영역 끝
        }
    }
}