이펙티브 자바 아이템 79. 과도한 동기하는 피하라. 이전 상황을 동기화를 하지 않았을때 문제가 생기는 상황을 다뤘고 이번에는 과도하게 동기화를 했을때 생기는 문제를 다룰것이다.
응답 불가와 안전 실패를 피하려면 동기화 메서드나 동기화 블록 안에서는 제어를 절대로 클라이언트에 양도하면 안된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 package com.github.sejoung.codetest.concurrent.over;import java.util.Collection;import java.util.Iterator;import java.util.Set;public class ForwardingSet <E> implements Set <E> { private final Set<E> s; public ForwardingSet (Set<E> s) { this .s = s; } public void clear () { s.clear(); } public boolean contains (Object o) { return s.contains(o); } public boolean isEmpty () { return s.isEmpty(); } public int size () { return s.size(); } public Iterator<E> iterator () { return s.iterator(); } public boolean add (E e) { return s.add(e); } public boolean remove (Object o) { return s.remove(o); } public boolean containsAll (Collection<?> c) { return s.containsAll(c); } public boolean addAll (Collection<? extends E> c) { return s.addAll(c); } public boolean removeAll (Collection<?> c) { return s.removeAll(c); } public boolean retainAll (Collection<?> c) { return s.retainAll(c); } public Object[] toArray() { return s.toArray(); } public <T> T[] toArray(T[] a) { return s.toArray(a); } @Override public boolean equals (Object o) { return s.equals(o); } @Override public int hashCode () { return s.hashCode(); } @Override public String toString () { return s.toString(); } }
1 2 3 4 5 6 7 8 9 10 package com.github.sejoung.codetest.concurrent.over;public interface SetObserver <E> { void added (ObservableSet<E> set, E element) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 package com.github.sejoung.codetest.concurrent.over;import java.util.ArrayList;import java.util.Collection;import java.util.List;import java.util.Set;public class ObservableSet <E> extends ForwardingSet <E> { private final List<SetObserver<E>> observers = new ArrayList <>(); public ObservableSet (Set<E> s) { super (s); } public void addObserver (SetObserver<E> observer) { synchronized (observers) { observers.add(observer); } } public boolean removeObserver (SetObserver<E> observer) { synchronized (observers) { return observers.remove(observer); } } private void notifyElementAdded (E element) { synchronized (observers) { for (SetObserver<E> observer : observers) { observer.added(this , element); } } } @Override public boolean add (E element) { boolean added = super .add(element); if (added) { notifyElementAdded(element); } return added; } @Override public boolean addAll (Collection<? extends E> c) { boolean result = false ; for (E element : c) { result |= add(element); } return result; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.github.sejoung.codetest.concurrent.over;import java.util.HashSet;public class Test1 { public static void main (String[] args) { ObservableSet<Integer> set = new ObservableSet <>(new HashSet <>()); set.addObserver((s, e) -> System.out.println(e)); for (int i = 0 ; i < 100 ; i++) { set.add(i); } } }
실행결과
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 Process finished with exit code 0
위에 코드는 정상적으로 보인다 여기서 23이면 자신을 구독해지 하는 코드 하나를 추가 하겠다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.github.sejoung.codetest.concurrent.over;import java.util.HashSet;public class Test2 { public static void main (String[] args) { ObservableSet<Integer> set = new ObservableSet <>(new HashSet <>()); set.addObserver(new SetObserver <>() { public void added (ObservableSet<Integer> s, Integer e) { System.out.println(e); if (e == 23 ) { s.removeObserver(this ); } } }); for (int i = 0 ; i < 100 ; i++) { set.add(i); } } }
실행결과
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Exception in thread "main" java.util.ConcurrentModificationException at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1042) at java.base/java.util.ArrayList$Itr.next(ArrayList.java:996) at com.github.sejoung.codetest.concurrent.over.ObservableSet.notifyElementAdded(ObservableSet.java:36) at com.github.sejoung.codetest.concurrent.over.ObservableSet.add(ObservableSet.java:74) at com.github.sejoung.codetest.concurrent.over.Test2.main(Test2.java:23) Process finished with exit code 1
위처럼 에러가 난다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.github.sejoung.codetest.concurrent.over;import java.util.HashSet;import java.util.concurrent.ExecutionException;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class Test3 { public static void main (String[] args) { ObservableSet<Integer> set = new ObservableSet <>(new HashSet <>()); set.addObserver(new SetObserver <>() { public void added (ObservableSet<Integer> s, Integer e) { System.out.println(e); if (e == 23 ) { ExecutorService exec = Executors.newSingleThreadExecutor(); try { exec.submit(() -> s.removeObserver(this )).get(); } catch (ExecutionException | InterruptedException ex) { throw new AssertionError (ex); } finally { exec.shutdown(); } } } }); for (int i = 0 ; i < 100 ; i++) { set.add(i); } } }
실행결과
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
위에 처럼 백그라운드 쓰레드를 사용하면 교착상태에 빠진다.
동기화 블럭을 바깥으로 옴기면
1 2 3 4 5 6 7 8 9 10 11 12 private void notifyElementAdded (E element) { List<SetObserver<E>> snapshot = null ; synchronized (observers) { snapshot = new ArrayList <>(observers); } for (SetObserver<E> observer : snapshot) { observer.added(this , element); } }
이렇게 다시 2번과 3번 테스트를 수행하면
2번 테스트 실행결과
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Process finished with exit code 0
3번 테스트 실행결과
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Process finished with exit code 0
위에 결과처럼 무리 없이 동작한다.
또 다른 방법으로 CopyOnWriteArrayList를 사용하는 방법이 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package com.github.sejoung.codetest.concurrent.over;import java.util.ArrayList;import java.util.Collection;import java.util.List;import java.util.Set;import java.util.concurrent.CopyOnWriteArrayList;public class ObservableSet <E> extends ForwardingSet <E> { private final List<SetObserver<E>> observers = new CopyOnWriteArrayList <>(); public ObservableSet (Set<E> s) { super (s); } public void addObserver (SetObserver<E> observer) { observers.add(observer); } public boolean removeObserver (SetObserver<E> observer) { return observers.remove(observer); } private void notifyElementAdded (E element) { for (SetObserver<E> observer : observers) { observer.added(this , element); } } @Override public boolean add (E element) { boolean added = super .add(element); if (added) { notifyElementAdded(element); } return added; } @Override public boolean addAll (Collection<? extends E> c) { boolean result = false ; for (E element : c) { result |= add(element); } return result; } }
위에 처럼 수행하고 2번 3번 테스트를 진행하면
2번 테스트 실행결과
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Process finished with exit code 0
3번 테스트 실행결과
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Process finished with exit code 0
기본 규칙은 동기화 영역에서는 가능한 일을 적게 하는것이다.
참조