List<String>에 "data1"를 add()하는 행위는 원자적 연산인 것처럼 보인다. int i=1; 처럼 값을 곧바로 적재하는 듯 보이기 때문이다.
컬렉션의 핵심을 직접 구현하면서 이미 익숙하겠지만, 자바의 리스트는 내부에서 배열을 사용하고 배열의 단점을 극복하기 위해 size를 유동적으로 체크하는 등 리스트에 데이터를 add 한다는 것은 다음과 같은 로직을 가진다.
@Override
public void add(Object e) {
elementData[size] = e;
size++;
}
이미 배웠듯 size++; 자체가 원자적 연산이 아니며 그 이전에 나타난 elementData[size] = e; 가 존재하기에 add 메서드는 원자적 연산으로 볼 수 없다.
그러므로 List의 add는 멀티스레드 환경에서 동기화 문제를 일으킨다. 동기화를 해결하기 위해
@Override
public synchronized int size() {
return size;
}
@Override
public synchronized void add(Object e) {
elementData[size] = e;
sleep(100);
size++;
}
@Override
public synchronized Object get(int index) {
return elementData[index];
}
위의 코드처럼 synchronized를 입힐 수도 있지만, 프록시 패턴을 사용할 수도 있다.
public class SyncProxyList implements SimpleList{
private SimpleList target;
public SyncProxyList(SimpleList target) {
this.target = target;
}
@Override
public synchronized int size() {
return target.size();
}
@Override
public synchronized void add(Object e) {
target.add(e);
}
@Override
public String toString() {
return "SyncProxyList{" +
"target=" + target +
'}';
}
@Override
public synchronized Object get(int index) {
return target.toString() + " by " + this.getClass().getSimpleName();
}
}

위와 같이 BasicList에 synchronized를 생성자로 하여금 외부에서 입혀주는 것이다. 이렇게 프록시 패턴을 사용하면 우리는 생성자에 원하는 리스트 구현체를 넘겨주어 synchronized된 리스트를 이용할 수 있게 된다.
프록시 패턴(Proxy Pattern) 은 객체지향 디자인 패턴 중 하나로, 어떤 객체에 대한 접근을 제어하기 위해 그 객체의 대리인 또는 인터페이스 역할을 하는 객체를 제공하는 패턴이다. 프록시 객체는 실제 객체에 대한 참조를 유지하면서, 그 객체에 접근하거나 행동을 수행하기 전에 추가적인 처리를 할 수 있도록 한다. (ex.스프링 AOP)
동기화 기법을 적용하는 것은 멀티쓰레드 환경에서 안정성을 가져다 주지만 속도는 느려진다. 즉 Thread Safe와 성능간의 trade-off 관계가 존재하는데, synchronized의 경우 사실은 조금 극단적인 Thread Safe이다.
메서드 단위에 걸어야 해서 메서드 내부의 임계영역이 아닌 부분을 구분할 수 없다는 점, 모니터 락을 사용하기 때문에 락의 체크, 락의 해제, 락의 획득 등의 과정이 반복적으로 필요하다는 점 등이 있다. 전자의 경우 ReentrantLock으로 개선해보았으며, 후자의 경우 CAS로 한정적으로 돌파할 수 있었다.
CopyOnWriteArrayList: ArrayList의 동시성 안전 버전이다. 내부적으로 배열을 사용하며, 쓰기 작업이 있을 때마다 배열의 복사본을 생성하여 동시성 문제를 방지한다. 읽기 작업이 빈번하고 쓰기 작업이 드물 때 유리하다.
CopyOnWriteArraySet: Set의 동시성 안전 버전으로, 내부적으로 CopyOnWriteArrayList를 사용하여 구현된다.
ConcurrentSkipListSet: TreeSet의 동시성 안전 버전으로, 정렬된 순서를 유지하며 Comparator를 사용할 수 있다. Skip List 자료구조를 기반으로 하여 높은 성능의 동시성 집합을 제공한다.
ConcurrentHashMap: HashMap의 동시성 안전 버전이다. 해시 버킷을 여러 부분으로 나누어 동시에 접근할 수 있도록 하여 높은 성능을 유지한다.
ConcurrentSkipListMap: TreeMap의 동시성 안전 버전으로, 정렬된 순서를 유지하며 Comparator를 사용할 수 있다. Skip List 자료구조를 기반으로 하여 높은 성능의 동시성 맵을 제공한다.
ConcurrentLinkedQueue: 비차단(non-blocking) 동시성 큐다. 내부적으로 링크드 노드를 사용하여 구현되며, 빠르고 효율적인 FIFO(선입선출) 큐다.
ConcurrentLinkedDeque: 비차단(non-blocking) 동시성 데크이다. Deque의 동시성 안전 버전으로, 양쪽 끝에서 삽입과 삭제가 가능한 큐다.
ArrayBlockingQueue: 크기가 고정된 블로킹 큐다. 큐의 크기를 설정할 수 있으며, 공정(fair) 모드를 사용할 수 있다. 공정 모드를 사용할 경우, 큐에 대한 접근이 공정하게 이루어지지만 성능이 저하될 수 있다.
LinkedBlockingQueue: 크기가 무한하거나 고정된 블로킹 큐다. 기본적으로 크기가 매우 크고, 고정 크기로 설정할 수도 있다. 이 큐는 데이터 삽입과 삭제가 독립적으로 이루어지며, 많은 경우 생산자-소비자 패턴에 적합하다.
PriorityBlockingQueue: 우선순위가 높은 요소를 먼저 처리하는 블로킹 큐다. 요소가 우선순위에 따라 정렬되어 있으며, FIFO(선입선출) 순서와는 다르게 처리된다. 이 큐는 요소의 우선순위를 비교하여 처리하기 때문에, 우선순위에 따라 작업을 조정할 수 있다.
SynchronousQueue: 데이터 자체를 저장하지 않는 블로킹 큐다. 생산자가 데이터를 추가하면, 소비자가 그 데이터를 받을 때까지 대기한다. 생산자와 소비자 간의 직접적인 핸드오프 메커니즘을 제공하며, 중간에 큐가 없이 생산자와 소비자가 직접 거래한다.
DelayQueue: 지연된 요소를 처리하는 블로킹 큐다. 각 요소는 지정된 지연 시간이 지난 후에야 큐에서 제거될 수 있다. 일정 시간이 지난 후에 작업을 처리해야 하는 스케줄링 작업에 적합하다.