java-squid/effective-java

[아이템 79] 과도한 동기화는 피하라

Closed this issue · 6 comments

[아이템 79] 과도한 동기화는 피하라

p422 0,23까지 출력한 다음... 실제로 그렇게 진행되지 않는다!

왜 그렇게 진행되지 않는 이유에 대해 적혀있는 데, 잘 이해가 안되네요.
같이 이야기 나눠보면 좋을 듯 합니다.

정작 자신이 콜백을 거쳐 수정하는 것 까지는 막지 못한다..

이 문구가 특히 어떤 말인지 감이 잘 오지 않네요

p424 자바 언어의 락은 재진입(reentrant)을 허용하므로 교착상태에 빠지지 않는다.

이 문구도 잘 모르겠네요..

p422 0,23까지 출력한 다음... 실제로 그렇게 진행되지 않는다!

왜 그렇게 진행되지 않는 이유에 대해 적혀있는 데, 잘 이해가 안되네요.
같이 이야기 나눠보면 좋을 듯 합니다.

정작 자신이 콜백을 거쳐 수정하는 것 까지는 막지 못한다..

이 문구가 특히 어떤 말인지 감이 잘 오지 않네요

답변

도움이 되실줄은 모르겠으나 저는 디버깅을 통해 하나하나 분석해봐서 발생하는 이유를 알아내긴했습니다.

ConcurrentModificationException ?

  • 해당 컬렉션 객체의 일관성에 대해 비 정상적인 환경으로 될 수 있을 경우 해당 Exception 이 발생함.
  • Effective Java 에서 나온 내용은 이제 Observable 이란 코드 기반으로 이루어지는데,

Effective Java 예제

  • 이제 main에서 ObservableSet 를 생성후 element 를 add 할때 원소가 Set 에 추가되면서 이제 addNotifyElement 구독자들에게 원소가 추가됬다는 사실을 알려주기 위해서 이제 callback 함수인 added 가 발생하는데, 근데 여기서 그 람다로 정의된 함수를 보면 아래와 같이 되어 있습니다.
set.addObserver(new SetObserver<>() {
		public void added(ObservableSet<E> set, E element){
				print(element); // 귀찮아서 이렇게 적었습니다.
				if(e == 23) 
					s.removeObserver(this);
		}
});

참고 SetObservers

@FunctionalInterface
public interface SetObserver<E> {
    //Call this function when ObservableSet added element
    void added(ObservableSet<E> set, E element);
}
  • 그니까 오류가 나는 이유를 위와 같은 상황에 빗대어 봤을때 이제 added 는 콜백함수이다. 근데 지금 하나의 스레드 자체는 observers 에 lock 을 걸고 순회중인데, 갑자기 외부에서 해당 observer 에 접근이 가능하게 된것이다. 그니까 현재 notifyElementAdded 가 block synchronized 구조 인데
public void notifyElementAdded(E element) {
        synchronized (observers) {
            for (SetObserver<E> observer : observers) {
                observer.added(this, element);
            }
        }
    }

저런 구조 자체가 added 라는 callback 함수의 내부까지 synchronized 가 잡히지 않기 때문입니다. 실제로 진행해보면 이걸 Debugging 으로 하나하나씩 잡아보면, 처음에 add 에 들어오고 조건문을 거쳐 notifyElementAdded 로 가게 되고, 그 이후 observers 에 lock 을 걸고 observers 를 순회하게 되는데 이때 observer 의 added 메소드로 가게됩니다. (아직 끝나기전) 그래서 지금 메소드 스택구조를 간단하게 그려보면

----------------------
|       remove       |
----------------------
|   removeObservers  | 
----------------------
|        added       |
----------------------
| notifyElementAdded | => observerslock 을 걸어주었음
----------------------
|         add        |
----------------------

인데 지금 added 에서 remove 를 호출하여 notifyElementAdded 에서 작업하고 있는 observers 에 대한 순회 도중 변경이 일어났음 따라서 ConcurrentModificationException 발생하고 종료됨.

발생이유 상세하게

우리의 경우 List 이므로 for-loop 는 Iterator 로 작동하는데 Iter 클래스는 AbstractList(or List 등등 Collection 구조에 따라다를것임) 는 처음에 modCount 변수를 자신의 지역변수인 expectedModCount 에 할당하는데 이는 modCount 에 해당 Collection 에 관한 변경사항에 따른 count 를 기록하는데 add / remove 등등 다수의 메소드에서 기록됨. 그래서 next() 를 하는데 기존 자신이 참조하고 있는 Collection 과의 변경사항에 대해 Checkt 를 하는데 이때 다르면 ConcurrentModificationException 을 발생시키는데 지금과 같은 경우는 앞에서 add 를 해줘서 modCount++ 을 해준 상태(현재 상태 1)일텐데, 마지막에 remove 가 작동하여 modCount 를 ++ (2)해줍니다. 하지만 이미 exptectModCount 는 1 이므로 다음 next() 를 하는과정에서 ConcurrentModificationException 이 발생합니다. 이거 이해안가시면 당일날 디버깅으로 보여드리겠습니다. 직접보고싶으시면 ArrayList.java 에 956 번째 줄 967 번째 줄 Break Point 로 잡고 확인하시면 됩니다.

아 956 번째 줄에 BreakPoint 잡고 보는것이 조금 더 모든 과정을 보여준다? 라고 볼수도 있을것 같습니다

조금더 직관적인 설명을 하자면

ArrayList.java 안의 private class Itr implements Iterator 을 계속해서 호출해 오는데 우리가 처음에 add 를 해서 expecteModCount 한 값이 들어가고, 그 다음에 이제 remove 가 콜백되어 modCount++ 이 되어 오류가 나는것이다 . 음 Enhanced for loop 도 아래와 상당히 흡사하게 돌아가거나 (같을 것이므로 아래 코드를 예시로 들면) 처음에 modCount 가 들어가는데 이때는 add 가 되어 1 이 들어가있을텐데 그 도중에 remove 가 발생되어 2가 될테고, 결국 마지막 if (modCount != expectedModCount) 에 걸려서 ConcurrentModificationException 을 발생시킨다.

public void forEach(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        final int expectedModCount = modCount;
        final Object[] es = elementData;
        final int size = this.size;
        for (int i = 0; modCount == expectedModCount && i < size; i++)
            action.accept(elementAt(es, i));
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }

p424 자바 언어의 락은 재진입(reentrant)을 허용하므로 교착상태에 빠지지 않는다.

이 문구도 잘 모르겠네요..

이건 해당 내용에 관한 글인데 저도 이해하는데 조금 어렵긴 한데 코드에 설명이 잘 되어있네요.
계속 읽어보고 따라해보면서 이해해봐야 할듯 합니다.

https://parkcheolu.tistory.com/24

@tmdgusya 예시코드를 작성해보고, 설명해주신 걸 보니까 이해가 좀 되네요! 감사합니다