Collection과 Multi thread
자바의 Collection 클래스 대부분은 thread-safety 하지 않다.
아래 예의 ArrayList는 thread-safety 하지 않다. 따라서 위 코드는 ConcurrentModificationException
이 발생한다.
참고로 이런 Multi thread 가 아니라도 ConcurrentModificationException은 발생할 수 있다 자세한 내용은 ConcurrentModificationException 포스팅을 참고할 것.
public class Main {
public static void main(String[] args) {
List<integer> list = new ArrayList<integer>();
new WriterThread(list).start();
new ReaderThread(list).start();
}
}
public class ReaderThread extends Thread {
private final List<integer> list;
public ReaderThread(List<integer> list) {
super("ReaderThread");
this.list = list;
}
public void run() {
while (true) {
// foreach문은 내부적으로 iterator를 사용한다.
for (int n : list) {
System.out.println(n);
}
}
}
}
public class WriterThread extends Thread {
private final List<integer> list;
public WriterThread(List<integer> list) {
super("WriterThread");
this.list = list;
}
public void run() {
for (int i = 0; true; i++) {
list.add(i); // list에 write
list.remove(0); // list에 write
}
}
}
WriterThread가 list를 변경시키기 때문에 ReaderThread의 foreach 문에서 예외가 발생한다.
이런 Collection의 동기화 문제를 해결하는 2가지 방법이 있다.
1. Collections.synchronizedList()
Collections.synchronizedList() 메서드를 이용하면 Collection 인스턴스를 동기화 시킬 수 있다.
동기화되면 Collection 클래스의 모든 메서드는 동기화되어 동작한다.
(내부 코드를 보면 parameter로 전달받는 list를 new SynchronizedList(list)
로 만들고 내부적으로 호출을 synchronized로 호출하게 된다.)
주의해야 할 점은 구현에 따라 이게 모든 동기화 문제를 해결한다고 생각하면 안된다는 것이다.
참고로 Collection의 인터페이스에 따라서 Collections.synchronizedList()
, Collections.synchronizedSet()
, Collections.synchronizedMap()
등이 있다.
1.1. 예 1
위의 코드를 예를 들면 아래와 같이 ReaderThread는 synchronized로 list를 동기화 처리 해야 한다.
final List<integer> list
= Collections.synchronizedList( new ArrayList<integer>() );
public class ReaderThread extends Thread {
private final List<integer> list;
public ReaderThread(List<integer> list) {
super("ReaderThread");
this.list = list;
}
public void run() {
while (true) {
// list 객체로 동기화
synchronized (list) {
for (int n : list) {
System.out.println(n);
}
}
}
}
}
WriterThread에서 list 객체의 add(), remove()는 동기화 되어 여러 쓰레드에서 동시에 접근할 수 없다. ReaderThread의 foreach문의 경우 내부적으로 Collection을 다룰때 iterator 객체로 다루기 때문에 Collection의 동기화와 상관이 없다. 따라서, 루프문을 돌때는 list 변화가 일어나지 않도록 루프문 전체를 list 객체로 synchronized를 건 것이다.
1.2. 예 2
final List<String> list
= Collections.synchronizedList(new ArrayList<String>());
final int nThreads = 2;
ExecutorService es = Executors.newFixedThreadPool(nThreads);
for (int i = 0; i < nThreads; i++) {
es.execute(new Runnable() {
public void run() {
while(true) {
try {
list.clear();
list.add("888");
list.remove(0);
} catch(IndexOutOfBoundsException ioobe) {
ioobe.printStackTrace();
}
}
}
});
}
위 예는 여전히 문제가 발생한다. list의 각 메서드 동작이야 동기화가 되겠지만 Thread 1이 list.clear() 한 직후 Thread 2가 list.remove(0)을 하는 순간 빈 list를 지우려 하기 때문에 예외가 발생한다.
따라서 아래와 같이 동작 전체를 synchronized로 묶어야 한다.
sychronized(list) {
list.clear();
list.add("888");
list.remove(0);
}
2. CopyOnWriteArrayList 클래스
java.util.concurrent.CopyOnWriteArrayList
클래스는 thread-safety 하다.
final List<Integer> list
= new CopyOnWriteArrayList<Integer>();
copy-on-write는 ‘write 할 때 copy 한다’는 의미.
컬렉션에 대하여 write(추가, 수정)를 할 때마다, 내부에 확보된 배열을 통째로 복사한다. 이렇게 통째로 복사를 하면 iterator를 사용하여 element들을 순서대로 읽어가는 도중에 element가 변경될 염려가 없으므로 ConcurrentModificationException가 발생하지 않는다.
Collections.synchronizedList()를 통해서도 thread-safety한 collection을 사용할 수 있겠지만 이 경우 read에 대해서도 동기화 되어 성능상 손해가 발생한다.
이 클래스는 read에 대한 성능상 손해가 없는 대신 write할때마다 배열을 통째로 copy하기 때문에 성능저하가 심하다.
즉, 적은 write와 잦은 read가 발생할때 좋은 클래스이다.
3. BlockingQueue
보통 생산자 - 소비자 패턴에서 활용되는 큐로 많이 사용된다.
이 큐는 멀티쓰레드환경에서 대표할만한 컬렉션이다.
소비자가 꺼내어 사용할동안 생산자는 멈춰있고, 생산자가 넣을동안 소비자는 멈춰있어야한다.
4. ConcurrentHashMap
HashTable은 java2에서 Collection framework가 나오기 이전부터 있었던 클래스로 기본적으로 모든 메서드가 synchronized 로 동기화 되어 있다.
이는 Collections.synchronizedMap()
을 이용해 Map을 동기화 시키는 경우도 마찬가지다.
이렇게 모든 동작이 동기화되어 있을 경우 이 객체를 참조하는 thread의 개수가 많아질수록 경쟁이 심해져 성능이 기하급수적으로 떨어진다.
반면에 ConcurrentHashMap 에서는 내부적에 여러개의 세그먼트를 두고 각 세그먼트마다 별도의 락을 가지고 있다. 따라서 여러 쓰레드에서 ConcurrentHashMap 객체에 동시에 데이터를 삽입, 참조하더라도 그 데이터가 다른 세그먼트에 위치하면 서로 락을 얻기 위해 경쟁하지 않아 성능이 좋다.
[참고 문서]
댓글남기기