util 패키지에 있는 컬렉션 프레임워크는 원자적인 연산을 제공??
여러 스레드가 동시에 접근해도 괜찮을까?? == Thread Safe
package chap48;
import chap35.list.ArrayList;
import chap35.list.List;
public class SimpleListMainV0 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
System.out.println(list);
}
}
public interface SimpleList {
int size();
void add(Object e);
Object get(int index);
}
package chap48;
import java.util.Arrays;
import static chap41.util.ThreadUtils.sleep;
public class BasicList implements SimpleList {
private static final int CAPACITY = 5;
private Object[] elements;
private int size = 0;
public BasicList() {
this.elements = new Object[CAPACITY];
}
@Override
public int size() {
return size;
}
//원자적 연산 아니다.
@Override
public void add(Object e) {
elements[size] = e;
sleep(100); //멀티 스레드를 만드는 쉽게 확인하는 코드
size++;
}
@Override
public Object get(int index) {
return elements[index];
}
@Override
public String toString() {
return Arrays.toString(Arrays.copyOf(elements, size)) + " size =" + size + " capacity=" + elements.length;
}
}
package chap48;
import chap35.list.ArrayList;
import chap35.list.List;
public class SimpleListMainV1 {
public static void main(String[] args) {
SimpleList list = new BasicList();
list.add("A");
list.add("B");
System.out.println(list);
}
}
단일 스레드로 진행했기 때문에 문제 없음
package chap48;
import static chap41.util.MyLogger.log;
//두개를 넣었는데 하나의 값만 넣어짐
public class SimpleListMainV2 {
public static void main(String[] args) throws InterruptedException {
//test(new BasicList());
//test(new SyncList());
//target에 basicList가 들어감
BasicList basicList = new BasicList();
SyncProxyList syncProxyList = new SyncProxyList(basicList);
//proxyList에 있는 add를 호출 -> synchronized를 건다
test(syncProxyList);
}
private static void test(SimpleList list) throws InterruptedException {
log(list.getClass().getSimpleName());
Runnable addA = new Runnable() {
//Thread A
@Override
public void run() {
list.add("A");
log("Thread-1 : List.add(A)");
}
};
Runnable addB = new Runnable() {
//Thread B
@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);
}
}

가정 1) 스레드 1,2가 동시에 수행 -> A 값이 들어가고 B가 들어갈때 A값을 변경
가정 2) 스레드 1,2가 동시에 연산 수행 -> 0으로 읽고 -> 최종 결과물 1
package chap48;
import java.util.Arrays;
import static chap41.util.ThreadUtils.sleep;
public class SyncList implements SimpleList{
private static final int CAPACITY = 5;
private Object[] elements;
private int size = 0;
public SyncList() {
elements = new Object[CAPACITY];
}
@Override
public synchronized int size() {
return size;
}
@Override
public synchronized void add(Object e) {
elements[size++] = e;
sleep(100);
size++;
}
@Override
public synchronized Object get(int index) {
return elements[index];
}
@Override
public synchronized String toString() {
return Arrays.toString(Arrays.copyOf(elements, size)) + " size =" + size + " capacity=" + elements.length;
}
}
package chap48;
import static chap41.util.MyLogger.log;
//두개를 넣었는데 하나의 값만 넣어짐
public class SimpleListMainV2 {
public static void main(String[] args) throws InterruptedException {
//test(new BasicList());
//test(new SyncList());
//target에 basicList가 들어감
BasicList basicList = new BasicList();
SyncProxyList syncProxyList = new SyncProxyList(basicList);
//proxyList에 있는 add를 호출 -> synchronized를 건다
test(syncProxyList);
}
private static void test(SimpleList list) throws InterruptedException {
log(list.getClass().getSimpleName());
Runnable addA = new Runnable() {
//Thread A
@Override
public void run() {
list.add("A");
log("Thread-1 : List.add(A)");
}
};
Runnable addB = new Runnable() {
//Thread B
@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);
}
}
안전한 임계영역을 설정해 줬기 때문에 하나의 스레드 add만 메서드 수행
그렇다면 모든 util에 있는 컬랙션들을 하나하나 수정??
이럴때 사용하는게 proxy! = 대신 처리해주는 자
package chap48;
public class SyncProxyList implements SimpleList{
private SimpleList target;
// target을 주입 -> 호출되는 대상이 target에 들어감
public SyncProxyList(SimpleList target) {
this.target = target;
}
@Override
public synchronized int size() {
return target.size();
}
//lock을 획득
@Override
public void add(Object e) {
//원본 메서드 호출
target.add(e);
//원본 메서드 반납
//lock 반납
}
@Override
public synchronized Object get(int index) {
return target.get(index);
}
@Override
public String toString() {
return target.toString() + " by " + this.getClass().getSimpleName();
}
}
이 클래스는 생성자를 통해 SimpleList target 을 주입 받는다. 여기에 실제 호출되는 대상이 들어간다
클래스의 역할은 모든 메서드에 synchronized 를 걸어주는 일 뿐이다. 그리고나서 target 에 있는 같은 기능을 호출.
package chap48;
import static chap41.util.MyLogger.log;
//두개를 넣었는데 하나의 값만 넣어짐
public class SimpleListMainV2 {
public static void main(String[] args) throws InterruptedException {
//test(new BasicList());
//test(new SyncList());
//target에 basicList가 들어감
BasicList basicList = new BasicList();
SyncProxyList syncProxyList = new SyncProxyList(basicList);
//proxyList에 있는 add를 호출 -> synchronized를 건다
test(syncProxyList);
}
private static void test(SimpleList list) throws InterruptedException {
log(list.getClass().getSimpleName());
Runnable addA = new Runnable() {
//Thread A
@Override
public void run() {
list.add("A");
log("Thread-1 : List.add(A)");
}
};
Runnable addB = new Runnable() {
//Thread B
@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);
}
}
변경 구조: 클라이언트 SyncProxyList (프록시) BasicList (서버)
정적 의존관계

simpleList 인터페이스 덕분에 BasicList, SyncList, SyncProxyList중에 어떤 것을
사용하든, 클라이언트인 test() 의 코드는 전혀 변경하지 않아도 된다.
런타임 의존관계

실제 런티임에 발생하는 인스턴스의 의존관계 = 런타임 의존관계
test(new BasicList()) 를 실행하면 BasicList(x001) 의 인스턴스가 만들어지면서 test() 메서드에 전달된다.
test() 메서드는 BasicList(x001) 인스턴스의 참조를 알고 사용하게 된다.


test() 메서드에서 스레드를 만들고, 스레드에 있는 run() 에서 list.add() 를 호출
프록시인 SyncProxyList 는 synchronized 를 적용한다. 그리고 나서 target 에 있는add() 를 호출
원본 대상인 BasicList(x001) 의 add() 가 호출
SyncProxyList에 있는add()로 흐름이 돌아온다. 메서드를 반환하면서 synchronized 를 해제
Proxy 정리
프록시는 내부에 원본을 갖고 있다. 그래서 프록시가 필요한 일부의 일을 처리, 다음에 원본을 호출하는 구조 형성, 여기서 프록시는 synchronized를 통한 동기화 작용
프록시 pattern
객체지향 디자인 패턴 중 하나로 어떤 객체에 대한 접근을 제어하기 위해 그 객체의 대리인 또는 인터페이스의 역할을 하는 객체를 제공하는 pattern. 프록시 객체는 실제 객체에 대한 참조를 유지, 그 객체에 접근 or 수행 하기 전에 추가적 처리
프록시 패턴의 주요 목적
접근 제어: 실제 객체에 대한 접근을 제한하거나 통제할 수 있다.
성능 향상: 실제 객체의 생성을 지연시키거나 캐싱하여 성능을 최적화할 수 있다.
부가 기능 제공: 실제 객체에 추가적인 기능(로깅, 인증, 동기화 등)을 투명하게 제공할 수 있다.
자료구조 처음부터 동기화를 적용하면?? -> 성능과 tradeOff 발생
컬렉션이 항상 멀티스레드 환경에서 적용되는 것은 아니기 때문에 단일 스레드일 경우 성능 Issue 발생
package chap48;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SynchronizedListMain {
public static void main(String[] args) {
List<String> list = Collections.synchronizedList(new ArrayList<>());
//List<String> list = new ArrayList<>(); //동기화 문제 발생
list.add("data1");
list.add("data2");
list.add("data3");
System.out.println(list.getClass());
System.out.println("list = " + list);
}
}
Collections 는 다음과 같이 다양한 synchronized 동기화 메서드를 지원한다. 이 메서드를 사용하면 List ,Collection , Map , Set 등 다양한 동기화 프록시를 만들어낼 수 있다.
synchronizedList()
synchronizedCollection()
synchronizedMap()
synchronizedSet()
synchronizedNavigableMap()
synchronizedNavigableSet()
synchronizedSortedMap()
synchronizedSortedSet()
동시성 컬렉션은 스레드 안전한 컬렉션, 고성능 멀티스레드 환경을 지원하는 다양한 동시성 컬렉션 클래스들을 제공. ex) ConcurrentHashMap , CopyOnWriteArrayList , BlockingQueue


LIST
package chap48;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class ListMain {
public static void main(String[] args) {
List<Integer> list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
System.out.println("list = " + list);
}
}
SetMain
package chap48;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CopyOnWriteArraySet;
public class SetMain {
public static void main(String[] args) {
Set<Integer> set = new CopyOnWriteArraySet<>();
set.add(1);
set.add(2);
set.add(3);
set.add(4);
System.out.println("set = " + set);
Set<Integer> skip = new ConcurrentSkipListSet<>();
skip.add(1);
skip.add(2);
skip.add(3);
System.out.println("skip = " + skip);
}
}
MapMain
package chap48;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class MapMain {
public static void main(String[] args) {
Map<Integer, String> map1 = new ConcurrentHashMap<>();
map1.put(1, "data1");
map1.put(2, "data2");
map1.put(3, "data3");
System.out.println("map1 = " + map1);
Map<Integer, String> map2 = new ConcurrentHashMap<>();
map2.put(1, "data1");
map2.put(2, "data2");
map2.put(3, "data3");
System.out.println("map2 = " + map2);
}
}