ㆍThread
- JVM이 시작되면 자바 프로세스가 시작이 됨. 이 프로세스 안에는 적어도 하나 이상의 쓰레드가 수행된다.
- 프로세스끼리는 공유되는 자원 없이 각각 따로 메모리를 할당해주어야한다. 즉, 많은 리소스가 필요하지만 쓰레드는 상대적으로 적은 리소스가 필요하다. 따라서, 쓰레드를 '경량 프로세스'라고도 부른다.
- 쓰레드를 생성하는 방법에는 크게 Runnable 인터페이스를 구현하는 것과, 이 인터페이스를 구현한 클래스인 Thread 클래스를 상속받는 두가지 방법이 있다. 이들은 모두 java.lang 패키지에 있다.
- Runnable 인터페이스나 Thread 클래스를 상속받은 클래스는 run() 메소드를 구현하여 쓰레드 시작 시 수행할 작업을 지정해주어야 한다. 쓰레드의 start() 메소드를 실행하면, 클래스에 구현된 run() 메소드가 실행된다.
- Thread 클래스를 상속받으면 해당 클래스의 객체 생성 후 바로 쓰레드 메소드를 사용할 수 있지만, Runnable 인터페이스를 상속받은 클래스는 new Thread() 생성자에 해당 객체를 추가해주어야 한다.
new Thread(runnable).start();
// Runnable 인터페이스를 상속받은 클래스
thread.start();
// Thread 클래스를 상속받은 클래스
- Thread 클래스를 상속받는 것이 쓰레드 메소드를 바로 사용할 수 있어 편하지만, 자바에서는 다중 상속이 불가능하기 때문에 다른 클래스를 상속받아야 한다면 Runnable 인터페이스를 구현하여 쓰레드 동작을 수행할 수 있다.
- 쓰레드는 sequential하게 실행되지 않는다. 쓰레드 실행 이후 각 쓰레드의 동작은 독립적으로 수행된다. 또한, 일반 쓰레드는 쓰레드의 동작이 끝날때까지 JVM이 기다리게되어, 프로세스가 종료되지 않는다. 하지만 데몬 쓰레드는 다른 일반 쓰레드가 동작하고 있는 것이 아니라면, 데몬 쓰레드의 동작 여부와 상관없이 프로세스가 종료된다.
Thread 생성자
- Thread 생성자의 매개변수에 따라, 쓰레드의 이름이나 쓰레드 그룹을 지정할 수 있다.
- 두, 세 번째 생성자처럼 Runnable 인터페이스를 상속받은 클래스를 쓰레드 객체로 생성할 수 있다.- Thread 객체 생성 시. 쓰레드 이름을 지정해주지 않으면 "Thread-n" (n은 정수) 로 자동 지정된다.
- Thread 클래스를 상속받을 때, 해당 클래스에 아무런 조치를 취하지 않으면 Thread 기본 생성자만 사용된다. 따라서 super 키워드를 통해 명시적으로 Thread의 생성자를 지정해주는 것이 좋다.
- run()이나 start() 메소드는 매개변수가 없는 메소드이므로, 쓰레드 수행 시 값을 넘겨주고 싶다면 생성자를 이용하여 처리할 수 있다.
sleep() 메소드 - 쓰레드를 지정한 시간동안 대기
- sleep(long millis) : 현재 실행중인 쓰레드를 매개변수로 넘어온 시간 (밀리초)만큼 대기한다.
- sleep(long millis, int nanos) : 현재 실행중인 쓰레드를 매개변수로 넘어온 시간 (밀리초) + (나노초)만큼 대기한다.
Thread.sleep(1000);
// 1초동안 대기
- sleep 메소드는 static 이므로 객체 생성없이 사용 가능.
join() 메소드 - 쓰레드의 동작이 끝날때까지 대기
- join() : 현재 실행중인 쓰레드가 종료될때까지 대기한다.
- join(long millis) : 현재 실행중인 쓰레드가 종료될때까지 최대 매개변수로 받은 밀리초만큼 대기한다.
- join(long millis, int nanos) : 현재 실행중인 쓰레드가 종료될때까지 최대 매개변수로 받은 밀리초 + 나노초만큼 대기한다.
ThreadSample thread = new ThreadSample();
thread.join();
// 해당 쓰레드가 종료될때까지 기다린 후 다음 문장 실행
wait() 메소드 - notify(), notifyAll() 메소드가 실행될때까지 대기 (Object 객체의 메소드)
- wait() : 쓰레드를 대기시킨다. notify, notifyAll 메소드가 실행되면 깨어난다.
- wait(long timeout) : 쓰레드를 일정 시간(밀리초)동안 대기시킨다. 파라미터로 넘겨진 시간 이후 쓰레드는 깨어난다.
- wait(long timeout, int nanos) : 쓰레드를 일정 시간(밀리초 + 나노초)동안 대기시킨다. 파라미터로 넘겨진 시간 이후 쓰레드는 깨어난다.
- notify() : Object 객체의 모니터에 대기하고 있는 단일 쓰레드를 깨운다.
- notifyAll() : Object 객체의 모니터에 대기하고 있는 모든 쓰레드를 깨운다.
/// 쓰레드 run 메소드, wait()
public void run() {
try {
synchronized (monitor) {
monitor.wait(); // 쓰레드를 wait()
}
System.out.println(getName() + " is notified");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//// 쓰레드 관리 메소드, notify()
public void notifyMethod() {
Object monitor = new Object();
WaitThread thread = new WaitThread(monitor);
try {
synchronized (monitor) {
monitor.notify(); // wait()중인 쓰레드를 깨움
}
Thread.sleep(100);
System.out.println("Thread State(after notify)="+thread.getState());
} catch(InterruptedException e) {
e.printStackTrace();
}
}
- sleep(), join(), wait() 메소드는 모두 쓰레드 대기와 관련이 있다. 쓰레드가 대기할 때 interrupt() 메소드가 실행될 수 있는데, interrupt() 메소드는 대기중인 쓰레드를 종료시키고 InterruptedException 예외를 발생시킨다. 따라서, 이와 같은 쓰레드를 대기시키는 메소드를 사용할 때 예외처리를 해주어야한다.
Thread 주요 메소드
- run() : 쓰레드 실행시 수행되는 메소드
- getId() : 쓰레드의 고유 id를 리턴 [return long]
- getName() : 쓰레드의 이름을 리턴 [return String]
- setName(String name) : 쓰레드의 이름을 지정
- getPriority(), setPriority(int) : 우선순위를 확인하고, 지정한다. [우선순위의 기본값은 5이다. 우선 순위와 관련된 3개의 상수가 있다. MAX_PRIORITY = 10, NORM_PRIORITY = 5, MIN_PRIORITY = 1 높을수록 우선순위가 높음.]
- isDaemon() : 쓰레드가 데몬인지 확인한다.
- isDaemon(boolean on) : 쓰레드를 데몬으로 설정한다. (true를 전달하면 놓으면 데몬, false를 전달하면 일반쓰레드로 바꿈)
- getStackTrace() : 쓰레드의 스택 정보를 확인. [return StackTraceElement[] ]
- getState() : 쓰레드의 상태를 확인한다. [return Thread.State - enum 상수]
- getThreadGroup() : 쓰레드의 그룹을 확인한다. [return ThreadGroup]
ㆍsynchronized
- 여러 쓰레드가 동일한 객체에 선언된 메소드에 접근하여 인스턴스 변수를 수정하려고 할 때, 변수 값이 꼬이는 경우가 발생.
- 쓰레드에 안전하게 사용하려면 synchronized를 사용해야함.
++ 이러한 동시성 문제의 해결 방법으로 synchronized, volatile, Atomic 클래스 등의 사용이 있다.
- synchronized 사용 방법은 두가지가 있다.
1. 메소드 자체를 synchronized로 선언하는 방법.
public synchronized void plus() {
amount++;
}
2. 메소드 내의 특정 문장만 synchronized 블록으로 감싸는 방법.
Object plusLock=new Object(); // 잠금처리를 위한 객체 생성
Object minusLock=new Object(); // 잠금처리를 위한 객체 생성
public void plus() {
synchronized(plusLock){ // 잠금처리를 하기 위한 plusLock 객체
amount++;
}
}
public void minus() {
synchronized(minusLock){ // 잠금처리를 하기 위한 minusLock 객체
amount++;
}
}
- 이처럼 synchronized 키워드를 사용하면 해당 키워드로 감싸진 코드는 동시에 여러 쓰레드가 수행할 수 없고, 하나의 쓰레드만 수행하게 된다. 따라서, 먼저 synchronized 메소드, 블록을 사용하는 쓰레드가 있다면 해당 쓰레드의 동작이 끝날때까지 기다리게 된다.
- synchoronized 블록을 사용할 때, 잠금 처리를 할 객체가 필요하다. 만약 잠금 처리를 한 객체가 같다면, 해당 synchronized 블록은 하나의 쓰레드에서만 수행할 수 있다. 따라서, 전혀 다른 메소드를 synchronized 블록 처리해야한다면, 다른 객체를 생성하여 잠금 처리해야한다.
- 메소드 전체를 synchronized 로 처리하면 동시성 문제가 발생하지 않는 작업도 다른 쓰레드의 작업이 끝날 때까지 기다려야하기 때문에 상황에 맞게 사용하는 것이 필요하다.
ㆍThread.State
- Thread 클래스에는 쓰레드의 상태를 나타내는 State라는 enum 클래스가 있다.
- NEW = 쓰레드 객체는 생성되었지만 실행되지는 않은 상태.
- RUNNABLE = 쓰레드가 실행중인 상태.
- BLOCKED = 쓰레드가 실행 중지인 상태, 모니터 락이 풀리기를 기다리는 상태.
- WAITING = 쓰레드가 대기중인 상태.
- TIMED_WAITING = 특정 시간동안만 쓰레드가 대기중인 상태.
- TERMINATED = 쓰레드가 종료된 상태.
ㆍThreadGroup
- ThreadGroup은 쓰레드의 관리를 용이하게 하기 위한 클래스이다.
- 쓰레드 그룹은 기본적으로 트리구조를 가진다.
ThreadGroup 주요 메소드
- activeCount() : 그룹에서 실행중인 쓰레드의 개수를 리턴.
- enumerate(Thread[] list) : 현재 그룹에 있는 모든 쓰레드를 매개변수로 넘어온 쓰레드 배열에 담는다.
- enumerate(Thread[] list, boolean recurse) : 현재 그룹에 있는 모든 쓰레드를 매개변수로 넘어온 쓰레드 배열에 담는다. boolean이 true 이면 하위 쓰레드 그룹의 쓰레드들도 포함한다.
- getName(), getParent() : 쓰레드 그룹의 이름을 리턴 / 부모 쓰레드 그룹을 리턴.
- setDaemon(boolean) : 쓰레드 그룹에 속한 쓰레드들을 데몬으로 지정한다.
- list() : 쓰레드 그룹의 상세정보 출력.
ThreadGroup group = new ThreadGroup("1st"); // 쓰레드그룹을 1st라는 이름으로 생성.
Thread thread1 = new Thread(group, sleep1); // 쓰레드 생성시 그룹 지정
Thread thread2 = new Thread(group, sleep2);
thread1.start();
thread2.start();
System.out.println("Gruop Name=" + group.getName());
System.out.println("Active count=" + group.activeCount());
group.list();
Thread[] ThreadList = new Thread[group.activeCount()];
int result = group.enumerate(tempThreadList);
// 그룹안에 있는 쓰레드들을 배열에 담음
++)
- 프로그램은 코드의 집합체, 쓰레드와 프로세스는 cpu core에서 실행하는 하나의 단위, 즉 실행 단위이다.
- 프로세스는 기본적으로 하나의 쓰레드를 가지고 있다.
- 동시성이란, 한 순간에 여러가지 일이 진행되는 것이 아니라 짧은 전환으로 여러가지 일을 동시에 처리하는 것처럼 보이는 것이다.
- cpu는 기본적으로 한 코어에서 여러 작업을 처리할 때 짧은 주기로 작업을 바꿔가면서 처리하는데 이것을 Context switching이라고 한다.
- 운영체제는 이러한 context switching을 위해 프로세스를 제어하기 위한 정보를 저장해야한다. 이것이 PCB(Process Control Block)이다. PCB는 프로세스 생성 시 만들어지며 주기억장치에 유지된다.
- PCB는 포인터, 프로세스 현재 상태를 담는 process State, 프로세스의 고유 번호를 담는 PID, 다음 명령어를 가리키는 Program Counter, 이전에 작업하던 내용 Registers, CPU 스케줄링 정보(우선순위, 최종 실행시각) 등으로 이루어져있다.
- 프로세스는 각각 Code(코드), Data(static, 전역변수), Heap(동적 메모리 영역), Stack(지역변수, 매개변수, 반환 값 등 일시적인 데이터)의 메모리 영역을 OS로부터 할당받는다.
- 쓰레드는 프로세스의 자원인 Code, Data, Heap 메모리 영역을 공유하고 독립적인 Stack 영역을 갖는다.
- 멀티쓰레드 => 한 프로세스 안의 여러 쓰레드는 Stack영역을 제외한 자원을 서로 공유한다. 따라서, 메모리를 효율적으로 사용하며 context switching이 일어날 때 '캐싱 적중률'이 올라간다. 즉, context switching 비용이 적다. 각 쓰레드는 서로 긴밀하게 연결되어있으며 한 쓰레드에 문제가 생길시 다른 쓰레드에도 영향이 간다. 또한 같은 데이터를 공유하기 때문에 데이터 동기화에 신경써주어야 한다.
- 멀티프로세스 => 여러 프로세스는 각 프로세스마다 독립적인 code, data, heap, stack 메모리 영역을 가지기 때문에 멀티쓰레드보다 메모리를 많이 차지한다. 또한 각 프로세스끼리 자원을 공유하지 않기때문에 IPC를 사용하여 프로세스끼리 통신을 해야하고 context switching 비용이 크다 (기존에 사용하던 자원을 내리고 새로운 자원을 가져와야하기 때문). 하지만, 멀티쓰레드와는 달리 각 프로세스가 독립적이기 때문에 한 프로세스에 문제가 생겨도 다른 프로세스에 미치는 영향이 적고, 동기화 작업에 신경을 덜 써도 된다는 장점이 있다.
참고
- 자바의 신 Vol.2 (이상민 저자.)
- https://devwithpug.github.io/java/java-thread-safe/
자바에서 동시성 문제를 해결하는 3가지 키워드
개요
devwithpug.github.io
- https://jhnyang.tistory.com/33
[운영체제]PCB (Process Control Block)란? PCB 정보 & Context Switching 문맥교환 & Overhead 오버헤드
운영체제 목차 프로세스의 정의와 프로세스 상태에 대한 이해를 기반으로 하고 있습니다. 헷갈리시는 분은 이전 포스팅 보고 오기 프로세스를 조금 어렵게(?) 이렇게 표현하기도 해요 프로세스
jhnyang.tistory.com