각각의 스레드가 독립적으로 데이터를 저장하고 관리할 수 있는 클래스.
각 스레드마다 개별적인 변수 공간을 할당하여, 공유 자원 없이, 안전하게 데이터를 유지할 수 있다.
get()을 호출하면 현재 실행 중인 스레드와 데이터 변환set()을 호출하면 현재 실행 중인 스레드와 데이터 변경
돌아와 인마...
암튼 마지막에 ThreadLocal.remove() 메소드를 호출해 데이터를 정리하게 된다고 한다...
멀티 스레드 환경에서의 동시성 문제
여러 스레드가 공유 자원에 접근하다보면 데이터 충돌이 발생해,
스레드마다 독립적인 데이터 공간을 가지(=공유 자원 없이 데이터 일관성을 유지할 수 있게 되며),
동기화synchronized없이 안전하게 멀티 스레드 프로그래밍이 가능하다고 한다.
내 뇌가 지금 교착 상태다
thread pool과 함께 사용할 때 주의해야 함.
ThreadLocal은 스레드의 내부 ThreadLocalMap에 데이터를 저장ThreadLocalMap이라는 해시맵이 존재하며, ThreadLocal은 여기에 데이터를 저장한다.ThreadLocalMap의 키는 ThreadLocal 객체 자체이며, 값은 저장된 데이터이다.
...잠깐 정리 좀 해야할 것 같다.
ThreadLocal<T> 객체를 만들고, 여기에 값을 지정.ThreadLocalMap에 저장된다.ThreadLocal 객체 자신, Map의 value는 내가 넣으라고 한 값.
threadLocal.set("A") 하면,{ (threadLocal, "A"( }{ (threadLocal, "B") }동일한 ThreadLocal 객체를 여러 스레드에서 공유해도,
Thread t1 = new Thread(() -> {
myLocal.set("A 값");
System.out.println(myLocal.get()); // 출력: A 값
});
Thread t2 = new Thread(() -> {
myLocal.set("B 값");
System.out.println(myLocal.get()); // 출력: B 값
});
t1.start();
t2.start();
저장 공간은 각 스레드마다 완전히 따로 있다.
ThreadLocal 데이터가 유지될 수 있다.
값이 남아돈다!!!
ThreadLocal은 스레드마다 고유한 저장공간
→ 스레드 풀은 스레드를 재활용해 작업을 처리하려 한다.
여기서 생기는 문제
ThreadLocal에 값을 넣음(set())
→ 작업이 끝나고,ThreadLocal의 값을 (remove()호출 없이) 그냥 둔 상태로 스레드가 다시 풀로 전환- 다음에 다른 작업이 이 스레드를 사용
→ 이전 값이 아직 남아 있음- 결국,
ThreadLocal값이 지워지지 않고 다른 작업에서 값이 섞이는 문제 발생.
때문에 작업이 끝난 뒤엔 반드시 remove()로 깨끗이 청소해야 한다.
ThreadLocal의 내부 구조 상,ThreadLocal 객체(Key)는 약한 참조ThreadLocal 객체를 더이상 코드에서 사용하지 않더라도,ThreadLocalMap 내부에 값이 남아있다면 GC(가비지 컬렉터)가 Value를 수거하지 못하게 된다.ThreadLocal의 객체는 없으나, 값이 메모리에 남아있는 메모리 누수가 발생하게 된다. ThreadLocal<MyBigObject> leak = new ThreadLocal<>();
leak.set(new MyBigObject());
// leak 변수를 더이상 코드에서 안 써도,
// 스레드의 ThreadLocalMap에는 값이 남아있다
}
해당 경우에도 반드시 remove()를 호출해 깨끗이 청소해야 한다.
HashMap은 여러 스레드가 동시에 값을 바꿀 때 동기화, 데이터 깨짐 등의 문제가 생길 수 있다.
ConcurrentHashMap은 멀티 스레드 환경에서 안전하게 값을 넣고 꺼낼 수 있게 해주는 클래스.
HashMap 전체를 한 번에 lock하지 않고,예시
// 여러 스레드가 동시에 접근해도 안전!
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
map.put("count", map.getOrDefault("count", 0) + 1);
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(map.get("count"));
HashMap으로 했다면 오류가 발생했을 것.
정리하던 도중 이상한 것을 발견했다.
ConcurrentHashMap은 참 열심히 사는 친구인 것 같았는데, 이자식은
map.put(null, "hi"); // ❌ NullPointerException!
map.put("hello", null); // ❌ NullPointerException!
동시성 제어에 혼란을 야기할 수 있기에 Null을 넣지 못하게 되어있다고 한다.
아니 어째 다들 하나씩 나사가 빠진 느낌이다.
HashMap | TreeMap | ConcurrentHashMap | |
|---|---|---|---|
| 정렬 | X | O (Key 오름차순) | X |
| 속도 | O(1) | O(log n) | O(1)에 가까움 |
| null | O | Key X, value O | X |
| 동기화 | X | X | O |
| 사용처 | 단일 스레드 | 정렬, 범위검색 | 멀티스레드, 동시접근 |
이렇다는 건가?
클래스들이 참 다양하게도 각자의 특징이 있구나 싶다.
자체적으로 스레드 풀을 만들어준다고 한다
헐
Thread thread = new Thread(() -> { ... });thread.start();에 익숙해지기도 전에 스레드 풀을 자동화해주는 인터페이스를 배우게 되어버렸다
execute(Runnable command): Runnable 작업을 실행
submit(Runnable task): Runnable 작업을 실행하고 Future<?> 반환
submit(Callable<T> task): Callable<T> 작업을 실행하고 결과를 Future<T>로 반환
shutdown(): 새로운 작업을 거부하고, 실행중인 작업 완료 후 종료
shutdownNow(): 실행 중인 작업을 강제종료, 즉시 스레드 풀 종료
===
ExecutorService executor = Executors.newFixedThreadPool(2);executor를 선언한 뒤
newFixed를 선언(ThreadPool의 크기는 2)
executor.execute() -> System.out.printIn(Thread.currentThread().getName());execute 메소드를 사용해 뒷부분에 실행할 메소드를 넣은 것
executor.shutdown();종료 처리
미리 일정 개수의 스레드를 생성해두고 필요할 때마다 빌려 쓰는 방식
1) 일정 개수의 스레드가 미리 생성됨
FixedThreadPool(n)
2) 사용자가 작업을 요청할 시, 대기 중인 스레드가 실행됨
3) 작업이 끝난 스레드는 풀로 반환되어 재사용됨
4) 스레드의 개수가 부족할 경우 새로운 작업은 대기 큐에 저장됨
5) shutdown()을 호출하면 더이상 새로운 작업을 받지 않고, 실행 중인 작업이 끝날 경우 스레드가 종료
쿠팡 물류센터 구조 같다.
스레드 친구들은 일주일에 두 번 이상 호출 당해도 주휴수당을 받지는 않겠지만...
newFixedThreadPool(int nThreads)
newCachedThreadPool()
newSingleThreadExecutor()
newScheduledThreadPool(int corePoolSize)
참 길다
예제 1)
FixedThreadPool 사용고정된 개수의 스레드를 유지하는 스레드풀
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
// 스레드 3개짜리 풀 생성
for (int i = 1; i <= 5; i++) {
int taskNum = i;
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " 작업 실행: " + taskNum);
});
}
// 3개의 스레드가 task를 맡아 작업 번호를 출력
executor.shutdown(); // 실행 완료 후 스레드 종료
}
}
예시 출력:
pool-1-thread-2 작업 실행: 2
pool-1-thread-3 작업 실행: 3
pool-1-thread-2 작업 실행: 4
pool-1-thread-1 작업 실행: 5
예제 2)
ScheduledThreadPool 사용일정 간격으로 반복작업을 하는 스레드풀
public class ScheduledThreadPoolExample {
public static void main(String[] args) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
// 3초 후에 한 번 실행
scheduler.schedule(() -> System.out.println("3초 후 실행"), 3, TimeUnit.SECONDS);
// 1초 후 실행, 이후 2초마다 반복 실행
scheduler.scheduleAtFixedRate(
() -> System.out.println("반복 실행"),
1, // initialDelay: 1초 뒤에 첫 실행
2, // period: 이후 2초마다 반복
TimeUnit.SECONDS
);
}
}
예시 출력:
홀수 초마다 "반복 실행" 출력
3초 째에는 "3초 후 실행" 출력
예제 3)
CatchedThreadPool 사용작업량이 일정하지 않고, 순간적인 요청이 많을 때
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
// 필요한 만큼 스레드를 즉시 생성
// 이전에 사용하던 스레드가 살아있을 경우 재사용,
// 오래 사용 안하면 알아서 제거
// 대기 큐 없이 곧바로 새 스레드를 생성하니 과부하에 주의
for (int i = 1; i <= 10; i++) {
int taskNum = i;
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 작업 실행: " + taskNum);
try {
Thread.sleep(2000);
// 10개의 스레드가 2초동안 정지
} catch (InterruptedException e) {
e.printStackTrace();
// 누군가 깨울 경우 에러 메시지 출력
}
});
}
executor.shutdown();
}
}
shutdown()과 shutdownNow()shutdown(): 기존 작업을 모두 마친 후 스레드 종료shutdownNow(): 실행 중인 작업을 즉시 중단하고 스레드 종료 public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.shutdown(); // 기존 작업은 다 끝내고 종료 준비
try {
// 5초 안에 모든 작업이 끝나지 않으면
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow(); // 남은 작업 즉시 중단
}
} catch (InterruptedException e) {
executor.shutdownNow(); // 예외 시에도 즉시 중단
}
}
}
나도 중단되고 싶다