동시성은 여러 작업이 겹치는 기간에 실행될 수 있음을 의미하며 동시에 실행되는 것이 아니라 CPU가 작업마다 시간을 분할해 동시에 실행되는 것처럼 보이게 하는 것이다.
동시성의 핵심 목표는 유휴 시간을 최소화하는 것이다.
둘 이상의 작업이 진행중일 경우에 데이터 무결성을 유지시켜줘야 한다.
사진 출처: https://www.baeldung.com/cs/concurrency-vs-parallelism
병렬성은 동일한 시간에 독립적인 작업을 실행할 수 있음을 의미한다. 동시성과 달리
여러 작업을 다른코어, 다른 프로세스, 별도의 컴퓨터 등에서 동시에 실행할 수 있다.
동시성과 병렬성의 작업 과정
프로세스란 단순히 실행중인 프로그램이라고 한다. 즉 사용자가 작성한 프로그램이 운영체제에 의해 메모리 공간을 할당받아 실행 중인 것을 말한다. 프로세스는 프로그램에 사용되는 데이터와 메모리 등의 자원 그리고 스레드로 구성된다.
스레드란 프로세스 내에서 실제로 작업을 수행하는 주체를 의미한다. 모든 프로세스에는 한 개 이상의 스레드가 존재하여 작업을 수행한다. 둘 이상의 스레드를 가지는 프로세스를 멀티스레드 프로세스라고 한다.
프로세스와 스레드의 차이점은 프로세스는 자신만의 주소공간(code,data, heap, stack)을 할당받아 프로세스는 다른 프로세스의 변수나 자료에 접근할 수 없다. 스레드는 stack만 따로 할당받고 나머지 영역은 다른 스레드끼리 서로 공유한다.
따라서 프로세스는 오류가 발생해서 프로세스가 강제 종료 되어도 다른 프로세스에 영향을 주지 않지만 스레드는 오류가 발생해서 종료된다면 같은 프로세스 내의 다른 모든 스레드도 강제로 종료된다. 이렇게 프로세스와 스레드는 개념의 범위부터 구조까지 모두 다르다.
📌 프로세스와 스레드, 멀티프로세스 멀티 스레드에 대해서
상태 | 열거 상수 | 설명 |
---|---|---|
객체 생성 | NEW | 쓰레드 객체가 생성, 아직 start() 메소드가 호출되지 않은 상태 |
실행 대기 | RUNNABLE | 실행 상태로 언제든지 갈 수 있는 상태 |
WAITING | 다른 쓰레드가 통지할 때까지 기다리는 상태 | |
일시 정지 | TIME_WAITING | 주어진 시간 동안 기다리는 상태 |
BLOCKED | 사용하고자 하는 객체의 락이 풀릴 때까지 기다리는 상태 | |
종료 | TERMINATED | 실행을 마친 상태 |
sleep()
wait()
sleep()은 현재 스레드를 잠시 멈추게 할 뿐 lock을 해제하지는 않지만
wait()은 락을 소유하고 있던 스레드가 락을 해제하며 WAITING 또는 TIME_WATING으로 상태가 바뀌게 된다.
스레드 풀이란 동시에 여러 작업을 효율적으로 실행 및 관리하기 위해 서버에서 만드는 스레드 모음이다. 스레드 풀을 이용하면 각 작업에 대한 새 스레드를 생성하는 대신 이미 생성된 스레드 풀에 있는 스레드를 재사용한다. 이는 성능과 리소스 관리에 도움이 된다.
스레드 풀 생성 방법
newCachedThreadPool()
스레드 수의 제한을 두지 않는 스레드 풀 방식으로, 새로운 스레드 시작 요청이 들어올 때마다 스레드를 하나씩 생성한다.
업무가 종료된 스레드들은 바로 사라지지 않고 1분동안 살아있다가 다른 작업 요청이 없다면 제거 된다. 반복되는 스타일의 작업요청이 들어올 경우 유용하다.
newFixedThreadPool(int nThreads)
최대 스레드를 10개까지 만드는 방식으로, 동시에 일어나는 업무의 양이 비교적 일정할때 사용한다.
ThreadPoolExecutor 객체 생성
단 하나의 스레드를 생성하는 방식으로 주로 스레드 작업 중에 예외상황이 발생할
경우 예외처리를 위한 스레드 생성용으로 많이 사용한다.
newScheduledThreadPool()
일정 시간마다 주기적으로 반복해야하는 스타일의 동시작업을 위한 스레드풀이다. Timer 클래스를 대체할 수 있는 스레드 풀 방식이다.
ForkJoinPool()
최초의 ForkJoinPool을 생성한 스레드에서 부모 스레드로 업무를 할당하고 큰 업무를 작은 업무 단위로 쪼개서 부모 스레드로부터 부모 스레드로 부터 처리로직을 복사하여 새로운 스레드에서 쪼개진 작은 업무를 수행시킨다. 업무가 완료되면 다시 부모 스레드에서 Join하여 값을 취합한다. 이후 최초의 스레드로 값을 리턴한다.
/ runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS; // 11100000 00000000 00000000 00000000
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
RUNNING : 새로운 테스크를 받고 큐에 집어 넣는 일을 실행
SHOUTDOWN : 새로운 테스크를 받지 않음, 이미 큐에 있는 테스크는 처리
STOP : 새로운 테스크를 받지 않음, 큐에 있는 테스크도 처리하지 않음, 현재 진행중인 테스크에 인터럽트를 건다
TIDYING : 모든 테스크를 소멸, TIDYING 상태로 전이되는 스레드는 terminated() 메소드를 실행 시킴
TERMINATED : terminated() 메서드가 완료 됨
외부에서 직접적인 접근은 불가능하고 isShutdown(), isTerminated() 등 이런 메서드를 통해 스레드 풀의 상태를 알 수 있다.
excute()
리턴 값이 없는 Runnable 객체를 작업 큐에 저장한다. 따라서 작업처리 결과를 받지 못한다. 작업 처리 도중에 예외가 발생화면 스레드가 종료되고 해당 스레드를 스레드 풀에서 제거한 뒤, 다른 작업처리를 위해 새로운 스레드를 생성한다.
submit()
Runnable 또는 Callable을 작업큐에 저장하고 Future 객체를 리턴한다.작업처리 결과를 받을 수 있다. 작업 처리 도중에 예외가 발생하면 스레드는 종료되지 않고 다음 작업을 위해 재사용 된다.
정리
excute()는 리턴 값이 없고 submit()은 리턴 값이 있으며 exctue()는 예외 발생 시 스레드를 새로 생성하고 submit()은 스레드가 재사용 된다는 차이점이 있다.
따라서 스레드 생성 오버헤더를 줄이기 위해 submit()을 사용하는 것이 좋다.