스레드는 옛날에 미리 공부해둔 경험이 있어 챕터의 글을 좀 더 재밌게 수월하게 읽을 수 있었던 것 같다. 아는 만큼 보인다는 말이 이런 말일까,, 아무튼 개념 정리는 지난번에 했었기 때문에 내가 제대로 몰랐던 사실 위주로 스레드를 기록하고자 한다!
둘 모두 운영 체제에서 실행되는 프로그램의 실행 단위
인데, 무슨 차이?!
프로세스 = 공장, 스레드 = 일꾼
왜냐구? 스레드는 프로세스의 자원을 활용해 실제로 작업을 수행하기 때문!
GPT에게 심심한 감사를 건네며..
정의:
프로세스
: 운영 체제로부터 자원을 할당받아 실행되는 독립적인 프로그램 단위
스레드
: 프로세스 내에서 실행되는 작은 실행 단위로, 하나의 프로세스 내에서 동시에 실행될 수 있는 코드의 흐름을 나타냅니다.
자원 공유:
프로세스
: 각각의 프로세스는 독립된 메모리 공간, 파일, 입출력 장치 등을 가지며, 다른 프로세스와는 메모리 공유가 일어나지 않습니다. 프로세스 간에 데이터를 주고받기 위해선 특별한 통신 메커니즘을 사용해야 합니다.
스레드
: 같은 프로세스 내에서 스레드들은 메모리 공간을 공유합니다. 스레드는 프로세스의 주소 공간, 데이터, 스택 등을 공유하여 서로간에 데이터를 주고받고 동시에 작업을 수행할 수 있습니다.
생성과 제거:
프로세스
: 프로세스는 운영 체제에 의해 독립적으로 생성되고 제거됩니다. 프로세스는 운영 체제로부터 자원을 할당받아 실행되며, 각각의 프로세스는 최소한 하나의 스레드(메인 스레드)를 가지고 있어야 합니다.
스레드
: 스레드는 프로세스 내에서 생성되고 제거됩니다. 한 프로세스 내에서 여러 개의 스레드가 동시에 실행될 수 있으며, 스레드는 하나의 프로세스에 속해있어야 합니다.
독립성:
프로세스
: 각각의 프로세스는 독립된 실행 단위로, 다른 프로세스의 영향을 받지 않고 독립적으로 실행됩니다. 각 프로세스는 독립된 주소 공간을 가지고 있어, 하나의 프로세스에 문제가 발생하더라도 다른 프로세스는 영향을 받지 않습니다.
스레드
: 스레드는 하나의 프로세스 내에서 실행되는 작은 실행 단위로, 같은 프로세스 내의 스레드는 서로 동시에 실행
오버헤드
프로세스
: 프로세스 간의 전환(context switching)은 상대적으로 오버헤드가 큽니다. 이는 각 프로세스가 독립된 메모리 공간을 가지기 때문에, 상태 정보를 저장하고 복원해야 하기 때문입니다.
스레드
: 스레드 간의 전환은 프로세스 간의 전환보다 오버헤드가 적습니다. 스레드는 같은 프로세스의 자원을 공유하므로, 상태 정보를 저장하고 복원하는데 필요한 작업이 적어집니다.
스레드의 개수가 결정되어있진 않으나, 프로세스의 메모리공간(호출스택)에 따라 개수가 제한된다.
위의 그림처럼 스레드는 5가지 상태로 나뉜다.
동기화
는 하나의 자원을 여러 개의 스레드가 공유하면서 서로의 결과에 영향을 주기 때문에 이를 방지하는 기법이다. (멀티스레드에서 발생할 수 있는 문제)
synchronized(객체 참조변수) {
...
}
여기서 지정된 객체는 이 블록의 실행이 끝나면 lock이 풀린다. 이때 두 스레드가 lock을 건 상태에서 서로의 lock이 풀리길 기다리는 교착상태에 빠지지 않도록 조심해야 한다!!
(2) 메서드에 lock 걸기
public synchronized void cal {
...
}
(ex. 계좌의 잔고가 0보다 작아지면 더 이상의 출금이 이뤄지지 않도록 막는 코드를 작성했으나, 스레드 간에 객체 공유 시 0보다 작아져 출금이 막아지기 전에 다른 스레드에서 출금을 하여 문제가 발생함)
Lock 인터페이스 (근데 자바의 정석에서는 다루지 않는다. JAVA 8 버전이라 그런가..?)
아래 세개의 클래스로 구현한다.
(1) ReentrantLock (일반적!)
(2) ReentrantReadWriteLock (읽기, 쓰기 Lock이 따로 있는)
(3) StampedLock (위에 낙관적인 Lock 기능 추가.. 그러나 알 필요 X)
wait()과 notify() (Object 클래스의 메서드로 어디에서든 사용 가능하다)
도대체 뭐가 좋냐 물으신다면, 우리가 식당 웨이팅 중이라고 가정해보자.
당신은 대기실에 앉아 기다리다가 자리가 나면 호출 받는 것이 좋은가, 아니면 무대뽀로 앞에 다같이 서서 기다리는 것이 좋은가? wait과 notify는 전자의 경우를 제공해준다.
여러개의 스레드가 동시에 실행된다고 하여 멀티 스레딩이라 불린다.
그러나 사실은, 한개의 CPU가 한가지 동작만 수행할 수 있기 때문에 아주 짧은 시간 안에 스레드들이 번갈아가며 실행되고 있어 멀티 스레드처럼 보이는 것이다.
(ex. 메신저 -> 채팅 하면서 파일도 다운로드 받는)
이때 스레드의 작업전환(Context Switching)에 지연되는 시간이 발생해 싱글 스레드의 실행시간과 비슷하거나 더 들어갈 수도 있다.
(** 작업 전환시에는 현재 진행중인 작업의 다음 실행위치,, 등의 정보를 전달한다)
그러면 도대체 장점이 무엇인고, 하니 지난번 스레드 포스팅에서 작성한 장점도 있지만, 여기서 강조하는 장점은 CPU 이외의 자원을 사용(ex. 사용자에게 입력을 받는)하는 경우에는 시간 낭비를 줄일 수 있다!!
** 그래서 main메서드가 종료되어도 다른 스레드는 아직 실행중일 수 있다..!
우리가 Runnable 인터페이스나, Thread 클래스의 run 메서드를 실행할 때, run이 아닌 start 메서드를 호출하고 있음을 볼 수 있었다. 이는 run이라는 메서드는 단순히 클래스에 속한 메서드를 호출할 뿐이기 때문이다(싱글스레드)! 그렇기 때문에 start메서드로 동작을 대신한다.
(start메서드는 JVM을 활용하여 스레드 그룹에 해당 스레드를 추가해 각자의 스레드에서 실행될 수 있도록 한다)
근데 사실 start()를 호출했다고 해서 이 스레드가 바로 실행되는 것은 아니다.
JVM의 스레드 스케쥴러가 정한 순서에 실행되기 때문에, 기다려야 한다..
(여기서 스레드 스케쥴러에 규칙이 있나?? 하고 찾아봤지만 별다른 수확은 없었고
스케쥴러가 불확실 하기 때문에 너무 의존하지 말라는 결론만 얻었다..
=> 스케쥴러의 이 특성때문에 자바가 OS 독립적이라고 하지만 스레드는 종속적이라고 불리우는 예시가 되었다)
= 서로 관련된 스레드를 그룹으로 다루기 위한 것.
//사용법이 궁금해 찾아와봤다.
ThreadGroup tg1 = new ThreadGroup("Group A");
Thread t1 = new Thread(tg1,new MyRunnable(),"one");
Thread t2 = new Thread(tg1,new MyRunnable(),"two");
Thread t3 = new Thread(tg1,new MyRunnable(),"three");
++ 지양하는 이유가 더 자세히 궁금하여 ChatGPT를 활용해 보았다..
유연성 부족: 스레드 그룹은 스레드를 계층 구조로 구성하여 그룹 내에서 스레드를 조작하고 관리하는 기능을 제공합니다. 그러나 이러한 계층 구조는 일반적으로 필요하지 않으며, 복잡성을 증가시킬 수 있습니다. 또한, 스레드 그룹은 동적인 스레드 관리에 제한을 가할 수 있어 유연성이 부족할 수 있습니다.
관리의 어려움: 스레드 그룹은 스레드의 생명주기 및 우선순위, 예외 처리 등을 관리하기 위한 메서드를 제공합니다. 하지만 스레드 그룹을 사용하면 스레드의 관리가 더 복잡해지고, 스레드 간의 의존성과 상호작용이 불분명해질 수 있습니다. 따라서 스레드 그룹 대신에 Executor 또는 스레드 풀과 같은 고수준의 스레드 관리 메커니즘을 사용하는 것이 더 효과적입니다.
이식성 문제: 스레드 그룹은 플랫폼마다 동작 방식이 다를 수 있습니다. 따라서 스레드 그룹을 사용하면 애플리케이션이 특정 플랫폼에 종속되고 이식성 문제가 발생할 수 있습니다. 이에 반해, 스레드 그룹을 사용하지 않는다면 애플리케이션 코드는 플랫폼 중립적인 구조를 유지할 수 있습니다.
대안의 존재: 스레드 그룹을 대체할 수 있는 다른 기능과 라이브러리가 존재합니다. 예를 들어, Java 5부터 도입된 Executor 프레임워크와 관련된 클래스들은 스레드의 실행과 관리를 더욱 쉽게 처리할 수 있게 해줍니다. 또한, 자바 컨커런트(Java Concurrency) 유틸리티 패키지는 스레드 관리와 동시성 프로그래밍을 위한 다양한 기능과 클래스를 제공합니다. 이를 사용하면 스레드 그룹을 사용하지 않고도 스레드의 실행과 관리를 더욱 쉽게 처리할 수 있습니다.
4-1. Executor 및 ExecutorService 인터페이스: Executor 프레임워크는 작업을 스레드에 할당하고 실행하는 메커니즘을 제공합니다. ExecutorService는 Executor의 하위 인터페이스로, 작업의 실행, 스레드 풀의 관리, 작업의 스케줄링 등을 보다 편리하게 처리할 수 있습니다.
4-2. 스레드 풀(Thread Pool): 스레드 풀은 사전에 생성된 스레드 집합으로 작업을 처리하는 데 사용됩니다. ExecutorService를 사용하여 스레드 풀을 생성하고 작업을 제출하면, 스레드 풀이 작업을 자동으로 할당하고 관리합니다.
4-3. 동기화(Concurrency) 컬렉션: java.util.concurrent 패키지에는 여러 동기화 컬렉션 클래스가 포함되어 있습니다. 이러한 컬렉션은 여러 스레드가 동시에 접근할 수 있는 환경에서 안전하게 데이터를 저장하고 조작할 수 있도록 지원합니다. 예를 들어, ConcurrentHashMap, ConcurrentLinkedQueue, BlockingQueue 등이 있습니다.
4-4. 동시성 유틸리티 클래스: java.util.concurrent 패키지에는 스레드 간의 협력적인 동작을 돕는 다양한 클래스가 있습니다. 예를 들어, CountDownLatch, CyclicBarrier, Semaphore 등이 있으며, 이러한 클래스를 사용하여 스레드의 동기화와 상호작용을 구현할 수 있습니다.
이러한 자바 컨커런트 유틸리티들은 스레드 관리와 동시성 프로그래밍을 더욱 효과적이고 안전하게 처리할 수 있는 방법을 제공합니다. 따라서 스레드 그룹보다는 이러한 기능과 클래스를 사용하는 것이 권장됩니다.
= 일반 스레드의 작업을 돕는 보조적인 역할 수행
setDaemon(boolean ~)
메서드는 반드시 start() 호출 전 실행돼야 함자바의 정석, 2nd Edition.
https://www.javatpoint.com/threadgroup-in-java