자바에서 컬렉션 프레임워크는 원자적인 연산을 제공할까? ArrayList
인스턴스에 여러 스레드가 동시에 접근해도 될까? 여러 스레드가 동시에 접근해도 괜찮은 경우를 스레드 세이프라고한다.
그렇다면 ArrayList
는 스레드 세이프일까? 이 부분을 자세히 알기위해 임시로 예제 리스트를 구현해 보겠다.
public class BasicList implements SimpleList {
private static final int DEFAULT_CAPACITY = 5;
private Object[] elementData;
private int size = 0;
public BasicList() {
elementData = new Object[DEFAULT_CAPACITY];
}
@Override
public int size() {
return size;
}
@Override
public void add(Object e) {
elementData[size] = e;
sleep(100); // 멀티스레드 문제를 쉽게 확인하는 코드 size++;
}
@Override
public Object get(int index) {
return elementData[index];
}
@Override
public String toString() {
return Arrays.toString(Arrays.copyOf(elementData, size)) + " size=" +
size + ", capacity=" + elementData.length;
}
}
add()
는 원자적 계산이기때문에 여러 스레드가 동시에 접근해도 아무런 문제가 없다public void add(Object e) {
elementData[size] = e;
sleep(100); // 멀티스레드 문제를 쉽게 확인하는 코드
size++;
}
size++
자체가 원자적인 연산이 아니라서 멀티스레드 상황에서 사용하려면 synchronized
, Lock
등을 사용해서 동기화를 해야한다.public class SimpleListMainV2 {
public static void main(String[] args) throws InterruptedException {
test(new BasicList());
}
private static void test(SimpleList list) throws InterruptedException {
log(list.getClass().getSimpleName());
// A를 리스트에 저장하는 코드
Runnable addA = new Runnable() {
@Override
public void run() {
list.add("A");
log("Thread-1: list.add(A)");
}
};
// B를 리스트에 저장하는 코드
Runnable addB = new Runnable() {
@Override
public void run() {
list.add("B");
log("Thread-2: list.add(B)");
}
};
Thread thread1 = new Thread(addA, "Thread-1");
Thread thread2 = new Thread(addB, "Thread-2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
log(list);
}
}
실행결과
09:48:13.989 [ main] BasicList
09:48:14.093 [ Thread-1] Thread-1: list.add(A)
09:48:14.096 [ Thread-2] Thread-2: list.add(B)
09:48:14.096 [ main] [B, null] size=2, capacity=5
size
는 2인데 데이터는 B 하나만 입력되어있다public void add(Object e) {
elementData[size] = e; // 스레드1, 스레드2 동시에 실행
sleep(100);
size++;
}
예측 상황
elementData[0]=A
,elemenetData[0]
의 값은 A가 된다size++
이 실행되어 size=1
elementData[0]=B
,elemenetData[0]
의 값은 B가 된다size++
이 실행되어 size=2
정리
add()
와 같은 연산은 마치 원자적인 연산처럼 느껴진다. 하지만 그 내부에서는 수 많은 연산들이 함께 사용된다. 배열에 데이터를 추가하고, 사이즈를 변경하고, 배열 을 새로 만들어서 배열의 크기도 늘리고, 노드를 만들어서 링크에 연결하는 등 수 많은 복잡한 연산이 함께 사용된다synchronized
, Lock
등을 사용해서 동기화 처리를 하면 된다프록시를 사용해서 프록시가 대신 동기화 기능을 처리할 수 있다.
public class SyncProxyList implements SimpleList {
private SimpleList target;
public SyncProxyList(SimpleList target) {
this.target = target;
}
@Override
public synchronized void add(Object e) {
target.add(e);
}
@Override
public synchronized Object get(int index) {
return target.get(index);
}
@Override
public synchronized int size() {
return target.size();
}
@Override
public synchronized String toString() {
return target.toString() + " by " + this.getClass().getSimpleName();
}
}
SyncPoxylist
는 BasicList
와 같이 SimpleList
인터페이스를 구현한다synchronized
를 걸어주는 일 뿐 이다synchronized
만 걸고, 그 다음에 바로 실제 호출해야 하는 원본 대상( target
)을 호출 한다
프록시 패턴
위의 방식이 바로 프록시 패턴이다. 프록시 패턴은 객체지향 디자인 패턴 중 하나로 어떤 객체에 대힌 접근을 제어하기 위해 그 객체의 대리인 또는 인터페이스 역할을 하는 객체를 제공해주는 패턴이다.