Java

자바언어에서 동기화의 어려움 (1)

[하마] 이승현 (wowlsh93@gmail.com) 2015. 5. 20. 13:53
"멀티쓰레드 개발은 언어 무관하게 무지 어렵다. 세계 최고 개발자의 할아버지가 와도 어렵다." 


요즘 "폴리글랏 프로그래밍" 이 유행하고있습니다. 여러개의 언어를 적재적소에 사용해서 생산성 및 품질

을 올리자 뭐 이쯤되겠지요. 설사 주력언어 이외에 다른언어를 전혀 사용하지 않더라도, 다른 언어를

공부하는것은 주력언어에 에 대한 이해의 폭을 상당히 넓혀주므로 틈틈히 다른 언어를 공부하는건 굉장히

바람직한 일이 될것입니다. 따라서 " 7 가지 언어를 7주에 마스터"  같은 책도 인기가 있는것이겠구요.


하지만 언어를 배운다는것은 정말 힘든일이라고 생각되는게,  지금 쓰고있는 "자바언어에서 동기화의

어려움" 를 읽어보면 아시겠지만, 해당 언어에 대한 경험이 많지 않다면 실수하기 쉬운 문제가 도처에

도사리고 있습니다. 그 이슈를 실전에서 대처하려면 ,실수에 의한 경험도 필요하고, 가끔은 소스의 내부를

철저히 조사해봐야하는 수고를 해야하는데, 언어를 배우는것도 힘든데 저런부분까지 신경쓰려면 고난의

행군은 각오해야할거 같습니다. 


필자도 경력의 대부분을 C++  언어와 함께 하였기때문에 , 최근에 자바,파이썬,자바스크립트등으로

개발할때 많은 실수를 하게되는데 그때마다  제대로된 제품하나 만들기가 정말 너무 어렵구나..

남들이 쉽게 생각할지는 몰라도 소프트웨어 개발이라는게 정말 빡이 세구나 하는걸 많이 느끼게 됩니다.


자 아래에 써내려갈 야그들은 아마 자바고수님들이라면 다 알고 계실 오래된 내용일거 같습니다. 

하지만 다시 이렇게 정리해 보는것은 저 처럼 자바에 익숙치 않거나 , 초중급분들에겐 충분히 가치있는 내용이

될거 같아서 중복이겠지만 다시 되풀이해 보는시간을 가지려합니다.  

그리고 아래 Actor / Akka  게시글과 연관성도 있을듯 하네요. (http://okky.kr/article/279263



1. Collections.synchronizedList 이야기 

보통 우리는 Vector 대신해서 ArrayList 를 사용하라는 말을 듣곤합니다.  Vector 는 동기화되어진 함수로 가득차있기때문에 싱글쓰레드 프로그램에서 효율적이지 않다라는거지요. 

따라서 멀티쓰레드 프로그램을 짤때 ArrayList 를 사용할 경우 , 쓰레드문제에 대해서 신경을 써줘야하는데 , 선택할 2가지방법은 ArrayList 의 함수를 사용할때마다 적절한 동기화를 직접처리해주거나 Collections.synchronizedList 과 같은 함수를 사용하는 방법이 있습니다.


Collections.synchronizedList 사용법은 다음과 같습니다.

List list = Collections.synchronizedList(new ArrayList());


일단 아 저렇게 쓰면 쓰레드문제는 이제 신경안써도 되겠군~ 아싸~~!!! 하는 순간에 뒤통수 제대로 맞는거지요. 저렇게 되면 일단 함수하나를 사용할때는 락이 걸려서 중복호출이 되지 않겠지만, 여러함수가 호출될때 문제가 생길수있습니다.

다음과 같이

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();

                }
            }
        }
    });
}

위의 코드를 실행하면 , Thread A 가 remove(0) 을 하는 순간에 Thread B 가 clear() 를 한다면 ,, 꽝~~
remove 할것이 없는데 remove 를 하려니 문제가 생길수밖에요.  이럴땐 

 synchronized (list) {

    list.clear();

    list.add("888");

    list.remove(0);

}

이렇게 함수들을 묶어서 동기화를 시켜줘야합니다. 
따라서 단지 Collections.synchronizedList  를 쓴다고해서 동기화 문제를 회피할수있지 않다는 얘기입니다.


2. Threadsafe Iteration & ConcurrentModificationException 이야기 

컬렉션 객체를 사용할때 우리는 동기화에 대해서 굉장히 불분명할때가 많은데요. 그리고 예상치 못한 문제가 생기기도 합니다. 얼마전에 생긴문제인데요. 

final List<String> list = new ArrayList<String>();

list.add("A");

list.add("B");

list.add("C");

for(String s : list) {

    if(s.equals("A")) {

        list.remove(s);

    }

}

위의 코드를 실행시키면 어떻게 될까요??  
ConcurrentModificationException 예외가 발생합니다. 정확히는 remove 끝내고 다시 위로 올라간후에
list 로 부터 다음것을 할당받는순간에 발생합니다.  (내부에서 next() 함수 호출될때) 

솔직히 저런 코드에서 이런 예외가 발생할수있다는걸 예상하는건  어렵습니다. 너무나 뻔해보이는
코드이고 설마 내부에서 해결해주지 않았겠어 하는 마음때문인데요. 저게 왜 예외가 발생하는지 알려면
소스 내부를 살펴봐야합니다.


내부를 살펴보겠습니다.!!

modCount 를 주의깊게 살펴봐주세요 !!!


ArrayList 의 add 함수 입니다.

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    ....
}

modCount 를 하나 증가시키고 있습니다.  위의 소스에서 add 를 3번하니깐 3이 되었겠네요. size 는 3 이 됩니다.

다음은  list.remove(s); 함수를 살펴보겠습니다.

private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

역시나 modCount 를 하나 증가시키고 있습니다. 이때 modeCount 는 4가 됩니다.  size 는 2로 줄어듭니다.

modCount가 늘어나고 size  가 줄어들기전인 처음 for  문이 시작되는 상황으로 돌아가서   for(String s : list)   부분을  살펴봅시다.

list 에서 s 객체를 가져오기 위해서  처음에는 Iterator 객체를 생성합니다.

이때 Iterator 객체는 list 객체의 값 (modeCount) 를 할당받습니다.


public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;

 expectedModCount = modCount;  이렇게  두 값은 3이 됨을 알수있습니다. CURSOR = 0  이 됩니다.


다음으로는  hasNext 를 호출하여 순회가능한지 확인합니다.

public boolean hasNext() {
return cursor != size;
}

size 는 3일것이고 CURSOR = 0  이기때문에 순회가능합니다.

이후에 modCount 가 4 가되었고. 다시 위(for  문 시작) 로 올라가봅시다.

자 대망의 next() 함수입니다. 순회가능하기때문에 해당차례의 객체를 얻기위하여 next() 를 호출합니다.

public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

자 여기 checkForComodification() 함수를 잘보시면 modCount != expectedModCount 다르면 예외~ 뿅!! 
위에 remove 함수쪽을 다시 올라가서 보시면 remove 하면서 modeCount 는 4가 되었고, size 는 2가 
되었는데, Iter 객체의 expectedModCount  는 여전히 3입니다. 먼가 수정이 이루어질때 이 변수는 변하지
않는겁니다. 그래서 예외가 발생하는거지요.  여기서  눈치챈분도 있겠지만 remove 뿐만아니라 add 해도 
예외가 발생합니다. 먼가 수정한것이기때문입니다.
그래서 저런 수정이 있을경우 대신해서 다음과 같이 작성합니다.

for(Iterator<String> iter = list.iterator(); iter.hasNext();) {

    String s = iter.next();

    if(s.equals("Test1")) {

        iter.remove();

    }

}

예외가 발생하지 않습니다.  이유는 다음 코드와 같이

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();

try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

iter 객체의 remove 에서는 expectedModCount = modCount; 를 같게끔 변경해주기때문입니다.

 

자 여기까지는 싱글쓰레드 얘기였습니다. 

이제 여기에 두개의 쓰레드가 동시에 컬렉션을 건드린다고 생각해봅시다...끔찍합니다. 그래서 akka 니 무상태 지향 코딩이니 같은것들이 나오는건가 봅니다.

설사 아래와 같이 foreach 를 쓰지않고 iterator 를 쓴다고 해도 멀티쓰레드상에서는 컬렉션의 아이템 수정이 가해지면 헬이됩니다.  여러모로 ConcurrentModificationException  예외는 멀티쓰레드 프로그래밍에서 중요한 예외가 될것입니다. 


3. Volatile  실패 이야기 

가끔 ConcurrentModificationException   예외는 의도치 않게 행동을 합니다.

public E next() {
    if (modCount != expectedModCount)
      throw new ConcurrentModificationException();
      .....
}

위의 소스를 보면 내부 상태가 불일치 할때 ConcurrentModificationException  를 던지기로 되있는데요. 
황당하게 어떤때는  예외를 안던지는 경우가 있습니다.  
A의 쓰레드가 next 를 호출하고 있고, B 쓰레드는 modCount 를 수정하고있습니다. 이때 위에 살펴본것과
마찬가지로 예외가 발생해야하는데 예외가 발생하지 않습니다. B 쓰레드가 수정한 modeCount 가 A 쓰레드
의 메모리공간에서는 변경되지 않았기때문인데요. 이때 떠오르는 생각이 , 최신의 변수만을 적용하고
싶을때 변수에 붙히는 키워드가 멀까요? 

그렇습니다. Volatile 인데요. (http://tutorials.jenkov.com/java-concurrency/volatile.html  참고) 
Volatile 이 붙어있는지 확인했더니. 없습니다. 조슈아 블로흐(이펙티브 자바 저자 & 자바 아키텍트) 가 정신을

딴데 팔고 API 를 개발했던것일까요?  그건 아닙니다.  volatile 은 동기화에 그닥 도움을 줄수없습니다. 
updateness 는 보장해도 atomicity 는 보장하지 않습니다.Volatile 을 붙혀놔도  operator (++) 가 atomic 하지 않습니다.


4. 그래서 어떻게 하면 되냐 OTL 

멀티쓰레드를 짤 때 문제되는 경쟁조건, 메모리가시성,데드락 등을 요리조리 피해서 코딩하는것의 어려움에 봉착되어 있을 때, 우리의 구원자 Dug Lea 가 나타나주셨습니다. akka 라이브러리를 만든사람인데요. 그 분은 

자바에 concurrent collection API 를 추가해 주셨습니다. (꽤 오래전 얘기죠?)  공부해야합니다. :-) 

http://javarevisited.blogspot.kr/2013/02/concurrent-collections-from-jdk-56-java-example-tutorial.html 

그 밖에  이 보다 더 상위 추상을 다루는 동시성 방법론들을 찾으러 출발하세요. 액터모형, STM 같은..


5. 주요 컬렉션 이야기

이 게시물이 너무 길어지기때문에 간단하게 정리합니다.

CopyOnWriteArrayList 

CopyOnWrite 가 말해주는것처럼 read (select) 시는 아무런 동기화 문제가 없기때문에 놔두고 

변경이 일어날경우 객체를 clone 해서 다루자는 전략입니다. 따라서 읽기행위가 많이 일어나는 

곳에서 사용하기 좋습니다. 위의 예제에서도 ArrayList 를 이거로 바꾸면 예외가 발생하지 않습니다.


BlockingQueue 

보통 생산자 - 소비자 패턴에서 활용되는 큐로 많이 사용된다. 사실 이야기는 이 큐는 멀티쓰레드환경에서 

대표할만한 컬렉션이라는 것이다. 전에 Actor / Akka 문서에 말한 큐같은것들이 대부분 이것으로 이루어져있다. 

소비자가 꺼내어 사용할동안 생산자는 멈춰있고, 생산자가 넣을동안 소비자는 멈춰있어야한다.

서로 쟁탈하면 선반은 망가질것이다.


ConcurrentHashMap

ConcurrentHashMap은 Map의 일부에만 Lock을 걸기때문에 HashTable과 synchronized Map 보다 

효율적인게 특징이다.


참고 

http://rayfd.me/2007/11/11/when-a-synchronized-class-isnt-threadsafe/ 

https://www.ibm.com/developerworks/community/blogs/738b7897-cd38-4f24-9f05-48dd69116837/entry/synchronized_collections_vs_concurrent_collections7?lang=en 

자바병렬프로그래밍 http://www.yes24.com/24/goods/3015162?scode=032&OzSrank=1