RequestCounter 클래스를 만드세요. hit(String path) 는 경로별 호출 수를 세고, snapshot() 은 Map<String, Integer> 를 반환합니다. 8개 스레드가 각각 /a, /b 를 10,000번씩 호출한 뒤 결과가 정확히 80,000 / 80,000 이 되도록 하세요. ConcurrentHashMap.merge 를 사용하세요.
import java.util.*;
import java.util.concurrent.*;
public class Ex1 {
static class RequestCounter {
private final ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();
void hit(String path) { counts.merge(path, 1, Integer::sum); } // merge 는 원자적
Map<String, Integer> snapshot() { return new TreeMap<>(counts); }
}
public static void main(String[] args) throws Exception {
RequestCounter c = new RequestCounter();
try (var pool = Executors.newFixedThreadPool(8)) {
for (int t = 0; t < 8; t++)
pool.submit(() -> { for (int i = 0; i < 10_000; i++) { c.hit("/a"); c.hit("/b"); } });
}
System.out.println(c.snapshot());
}
}
// 출력:
// {/a=80000, /b=80000}외부 API 호출을 흉내 내는 String fetch(int id) (200ms sleep 후 "item-" + id 반환) 를 20번 호출해야 합니다. (1) 순차 호출 (2) 가상 스레드로 병렬 호출 두 가지를 구현하고, 각각의 소요 시간이 약 4초 vs 약 0.2초임을 출력하세요. 결과 리스트는 id 순서를 유지해야 합니다.
import java.util.*;
import java.util.concurrent.*;
public class Ex2 {
static String fetch(int id) {
try { Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return "item-" + id;
}
public static void main(String[] args) throws Exception {
long t0 = System.currentTimeMillis();
List<String> seq = new ArrayList<>();
for (int i = 0; i < 20; i++) seq.add(fetch(i));
System.out.println("sequential ~" + (System.currentTimeMillis() - t0) / 1000 + "s, " + seq.size());
t0 = System.currentTimeMillis();
List<Future<String>> fs = new ArrayList<>();
try (var ex = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 20; i++) { int id = i; fs.add(ex.submit(() -> fetch(id))); }
}
List<String> par = new ArrayList<>();
for (Future<String> f : fs) par.add(f.get()); // submit 순서 = id 순서
System.out.println("parallel ~" + (System.currentTimeMillis() - t0) / 1000 + "s, " + par.get(19));
}
}
// 출력:
// sequential ~4s, 20
// parallel ~0s, item-19BoundedBuffer<T> 를 ReentrantLock 과 두 개의 Condition(notFull, notEmpty) 으로 구현하세요. put 은 가득 차면 대기, take 는 비면 대기합니다. 용량 2 버퍼에 생산자 1개가 1~5 를 넣고 소비자 1개가 꺼내 합계 15 를 출력하세요.
import java.util.*;
import java.util.concurrent.locks.*;
public class Ex3 {
static class BoundedBuffer<T> {
private final Deque<T> items = new ArrayDeque<>();
private final int capacity;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
BoundedBuffer(int capacity) { this.capacity = capacity; }
void put(T t) throws InterruptedException {
lock.lock();
try {
while (items.size() == capacity) notFull.await(); // while: 허위 깨어남(spurious wakeup) 대비
items.addLast(t);
notEmpty.signal();
} finally { lock.unlock(); }
}
T take() throws InterruptedException {
lock.lock();
try {
while (items.isEmpty()) notEmpty.await();
T t = items.pollFirst();
notFull.signal();
return t;
} finally { lock.unlock(); }
}
}
public static void main(String[] args) throws Exception {
BoundedBuffer<Integer> buf = new BoundedBuffer<>(2);
int[] sum = {0};
Thread producer = new Thread(() -> { try { for (int i = 1; i <= 5; i++) buf.put(i); } catch (InterruptedException e) {} });
Thread consumer = new Thread(() -> { try { for (int i = 0; i < 5; i++) sum[0] += buf.take(); } catch (InterruptedException e) {} });
producer.start(); consumer.start();
producer.join(); consumer.join();
System.out.println("sum = " + sum[0]);
}
}
// 출력:
// sum = 15