참조 :https://docs.oracle.com/javase/8/docs/api/index.html?java/util/concurrent/package-summary.html
동시성에 관련되어 문제를 해결하다가 java.util.concurrent 패키지에서 자주 쓰이는 클래스를 정리하게 되었다.
패키지에는 컬렉션을 포함하여 다음과 같은 도구들도 포함이 되어있다.
execute(Runnable task): 작업을 스레드 풀에 제출. → 결과 반환 xsubmit(Callable<T> task / Runnable task): 작업 제출 후 Future로 결과 반환.Future 객체란 : 비동기 작업의 결과를 나타내는 객체이다. 작업이 완료 되었는지 확인하거나, 완료될 때까지 기다렸다가 결과를 가져올 수 있다.shutdown(): 새로운 작업 수락 중단.shutdownNow(): 현재 실행 중인 작업 중단.execute 또는 submit으로 제출됨.submit 으로 제출된 객체는 Future 객체에 결과가 반환
ExecutorService executor = new ThreadPoolExecutor(
2,// corePoolSize4,// maximumPoolSize60,// keepAliveTime
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10)// 작업 큐
);
executor.execute(() -> {
System.out.println("Task executed by thread: " + Thread.currentThread().getName());
});
executor.shutdown();
Future의 확장: 결과를 처리하는 콜백 제공.Future는 작업 완료 여부를 기다리기 위해 get()을 호출해야 하지만, CompletableFuture는 작업 완료 후 콜백을 등록하여 비동기로 처리할 수 있다.
// Future 방식
Future<String> future = executor.submit(() -> "Hello");
String result = future.get(); // 결과를 기다리며 블로킹 발생
// CompletableFuture 방식
CompletableFuture.supplyAsync(() -> "Hello")
.thenAccept(result -> System.out.println(result)); // 결과 준비 시 실행
thenApply, thenAccept, whenComplete.supplyAsync(Supplier<T>): 비동기 작업 실행 후 값을 반환.thenApply(Function<T, R>): 결과를 변환.thenAccept(Consumer<T>): 결과를 소비.exceptionally(Function<Throwable, ? extends T>): 예외 처리.thenCombine: 두 작업의 결과를 조합.allOf: 여러 작업이 모두 완료될 때 실행.anyOf: 여러 작업 중 하나라도 완료되면 실행.exceptionally: 예외 발생 시 대체 결과를 제공.handle: 성공/실패 여부에 따라 각각 처리.ForkJoinPool.commonPool()을 사용해 작업 실행CompletableFuture는 내부적으로 스레드 안전한 방식으로 상태를 관리합니다.java
코드 복사
List<String> sharedList = Collections.synchronizedList(new ArrayList<>());
CompletableFuture.runAsync(() -> sharedList.add("Task1"));
CompletableFuture.runAsync(() -> sharedList.add("Task2"));
해결 방법:
Concurrent 컬렉션을 사용하는 방식으로 동시성 문제를 해결해야 합니다.java
CompletableFuture.supplyAsync(() -> {
return "Hello";
}).thenApply(result -> {
return result + " World!";
}).thenAccept(finalResult -> {
System.out.println(finalResult);
});
get)은 동시성 문제를 일으키지 않습니다.volatile 변수에 값을 쓰는 작업은 해당 값을 읽는 작업 이전에 반드시 수행됩니다.put,remove)은 데이터 변경을 동반하므로 부분 잠금(세그먼트 잠금)이 적용됩니다. 이는 전체 맵을 잠그지 않고 충돌 가능성이 있는 특정 버킷(bucket)만 잠금 처리하여 동시성을 높입니다.Compare-And-Swap(CAS) 연산을 사용해 락 없이 빠르게 시도.synchronized 키워드를 통해 잠금 수행.put: 지정된 키와 값을 삽입. 키가 이미 존재하면 값을 덮어씀.putIfAbsent: 지정된 키가 없을 때만 값을 삽입.computeIfAbsent: 키가 없으면 연산을 수행한 결과를 삽입.computeIfPresent: 키가 존재하면 연산을 수행하여 값을 업데이트.replace: 기존 값을 새로운 값으로 대체.replace(K key, V oldValue, V newValue): 기존 값이 특정 값일 때만 대체.merge: 기존 값과 새 값을 병합 후 저장.getOrDefault: 키가 없을 경우 기본값 반환.remove: 지정된 키와 값을 제거.replaceAll: 모든 값을 연산 결과로 대체.java
코드 복사
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
map.computeIfAbsent("key", k -> 0);
System.out.println(map.get("key"));// 출력: 1
wait()를 호출하여 대기.notify() 호출.wait()를 호출하여 대기.notify() 호출.offer: 큐의 끝에 요소 추가.poll: 큐의 앞에서 요소 제거.peek: 큐의 앞 요소 반환(제거하지 않음).java
코드 복사
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
queue.offer("Task1");
System.out.println(queue.poll());// 출력: Task1
acquire로 허가 요청, release로 반환.acquire: 허가를 얻기 위해 대기.release: 허가 반환.availablePermits: 남은 허가 수 반환.java
코드 복사
Semaphore semaphore = new Semaphore(2);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println("Accessing resource");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release();
}
}).start();
}
put: 큐가 가득 차면 대기 후 추가.take: 큐가 비어 있으면 대기 후 제거.offer: 큐가 가득 차면 실패.ReentrantLock과 Condition을 사용. Java의 ReentrantLock은 synchronized를 대체하거나 보완하기 위한 고급 동기화 도구로, 더 정교한 제어와 기능을 제공합니다. Condition은 ReentrantLock과 함께 사용되는 객체로, 대기(wait)와 알림(notify) 메커니즘을 구현합니다. Condition 은 synchronized의 wait()와 notify()를 대체하며, 더 유연한 대기-알림 구조를 제공합니다.java
코드 복사
BlockingQueue<String> queue = new ArrayBlockingQueue<>(2);
new Thread(() -> {
try {
queue.put("Task1");
System.out.println("Task1 added");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
new Thread(() -> {
try {
System.out.println(queue.take());// Task1 출력
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
각 상황에 맞는 도구를 사용하면 안정적이고 효율적인 동시성 프로그래밍을 구현할 수 있습니다.