-
Thread-Safe: Multi-Thread 환경에서 동시성을 제어하기Programming/Java 2021. 10. 17. 00:19
https://2jinishappy.tistory.com/323
Thread-Safe
Java 환경에서는 개발자가 쉽게 다중의 Thread를 생성하고 사용할 수 있다. 여러 쓰레드의 바이트 코드를 동시에 실행시키는 멀티 쓰레딩은 애플리케이션 성능을 향상시킬 수 있지만, Resource를 공유하기 때문에 안전한 접근 방식이 필요하다.
즉, Multiple Thread를 사용할 때 에는 Thread-Safe하게 사용해야 한다.대부분의 경우 멀티 쓰레드 애플리케이션의 오류는 여러 쓰레드 간에 자원을 잘못 공유했을 때 발생한다.
Stateless Implementations
public class MathUtils { public static BigInteger factorial(int number) { BigInteger f = new BigInteger("1"); for (int i = 2; i <= number; i++) { f = f.multiply(BigInteger.valueOf(i)); } return f; } }
factorial()
메서드는 상태를 저장하지 않고, 입력 값이 같을 경우 반드시 동일한 값을 반환한다.이 방법은 메서드의 실행이 외부 상태에 의존하지 않으며, 상태 필드를 변화시키지도 않기 때문에 멀티 쓰레드 환경에서도 안전하게 호출할 수 있다.
즉, 모든 쓰레드가 안전하게factorial()
메서드를 호출할 수 있고, 서로 간섭하지 않으며 원하는 예상 결과를 얻어낼 수 있다.stateless하게 구현하면 쉽게 thread-safe를 달성할 수 있다.
Immutable Implementations
서로 다른 쓰레드들 간에 상태를 공유해야 한다면, immutable하게 만듦으로써 thread-safe class를 만들 수 있다.
클래스 인스턴스가 생성되고 나면 내부 상태를 변경할 수 없는 것을 immutable 하다고 한다.
immutable 클래스를 만드는 방법은 모든 필드를 private, final로 만들고 setter를 제거하면 된다.
public class MessageService { private final String message; public MessageService(String message) { this.message = message; } // standard getter }
MessageService
클래스는 (하나밖에 없지만) 필드를 모두 private final로 등록하고 setter를 제거했다. 따라서 클래스 인스턴스가 생성될 때 이외에는 필드의 값을 쓸 수 없다.'클래스가 immutable 하다 👉 thread-safe 하다'면 이도 성립할까?
즉, '클래스가 mutable 하다 👉 thread-safe 하지 않다'일까?immutable하게 구현하지 않아도 여러 쓰레드가 MessageService 클래스에 대해 read-only 액세스 권한만 있어도 thread-safe 하다고 할 수 있다.
불변성만이 thread-safety를 달성하는 유일한 방식은 아니라는 뜻이다.
Thread-Local Fields
OOP에서는 객체가 필드를 이용해서 상태를 표현하고 메서드를 이용해서 행위를 나타내야 한다.
상태를 유지해야 할 때 필드를 쓰레드 로컬화해서 쓰레드 간 상태를 공유하지 않게 하면 thread-safe 클래스를 만들 수 있다.
Thread 클래스를 상속받은 클래스에서 필드를
private
로 선언하면 해당 필드를 thread-local로 만들 수 있다.public class ThreadA extends Thread { private final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); @Override public void run() { numbers.forEach(System.out::println); } }
이 예시에서 클래스 인스턴스를 생성하면 해당 인스턴스만의 고유한 상태를 가질 수 있고, private 제어자를 사용하기 때문에 다른 쓰레드와 공유하지 않는다. 따라서 이 클래스는 thread-safe하다.
public class StateHolder { private final String state; // standard constructors / getter } public class ThreadState { public static final ThreadLocal<StateHolder> statePerThread = new ThreadLocal<StateHolder>() { @Override protected StateHolder initialValue() { return new StateHolder("active"); } }; public static StateHolder getState() { return statePerThread.get(); } }
private 키워드를 사용하지 않아도
ThreadLocal
인스턴스를 할당해서 쓰레드 로컬 필드를 만들 수 있다.이 방법에서 각각의 쓰레드들은 constructor/getter를 통해 쓰레드 로컬 필드에 접근한다. 그리고 고유한 상태를 가질 수 있게 필드를 초기화해 복사한다.
Synchronized Collections
Collection 프레임워크에 포함된 Synchronized Wrapper Set을 통해 Thread-safe한 Collection을 만들 수 있다.
Collection<Integer> syncCollection = Collections.synchronizedCollection(new ArrayList<>()); Thread thread1 = new Thread(() -> syncCollection.addAll(Arrays.asList(1, 2, 3, 4, 5, 6))); Thread thread2 = new Thread(() -> syncCollection.addAll(Arrays.asList(7, 8, 9, 10, 11, 12))); thread1.start(); thread2.start();
synchronized된 collection은 동기화를 위해 locking을 사용한다.
즉, 한 번에 한 쓰레드만 메서드에 접근할 수 있고, 나머지 쓰레드는 첫 쓰레드의 lock이 풀릴 때 까지 blocked된다.
따라서 synchronize 방식은 수행을 직렬화 할 수 있어서 성능 저하의 원인이 된다.
Concurrent Collections
위의 synchronized collection를 사용하는 대신 concurrent collection을 사용해도 thread-safe collection을 만들 수 있다.
Java는
ConcurrentHashMap
처럼 여러 concurrent collection을 포함하는java.util.concurrent
패키지를 제공한다.Map<String,String> concurrentMap = new ConcurrentHashMap<>(); concurrentMap.put("1", "one"); concurrentMap.put("2", "two"); concurrentMap.put("3", "three");
동기화 방식과 달리 동시 방식은 데이터를 세그먼트로 분할하여 thread-safe를 구현한다.
ConcurrentHashMap에서는 여러 쓰레드가 서로 다른 맵 세그먼트에서 lock을 획득할 수 있기 때문에 동시에 Map에 액세스 하는 것이 가능하다.
synchronized 방식과 concurrent 방식 모두 Collection을 Thread-safe하게 만드는 것이고, 내부 Elements를 Thread-Safe하게 만드는 것은 아니다 ❗❗
Synchronized Methods
앞에서 소개한 접근 구현 방식들은 collection, data type에 적용할 수 있는 방법이다. 그보다 더 큰 범주에서 제어하기 위해 synchronized method를 만들어서 thread-safe를 구현할 수 있다.
synchronized method는 한 번에 하나의 쓰레드만 동기화된 메서드에 접근 할 수 있도록 허용하고, 다른 쓰레드는 해당 메서드의 접근 권한이 없다.
다른 쓰레드는 첫 번째 쓰레드의 수행이 완료되었거나 메서드가 예외를 throw할 때 까지 접근이 차단된 상태를 유지한다.
public synchronized void incrementCounter() { counter += 1; }
synchronized
키워드를 붙이면 동기화 메서드를 만들 수 있다.이 예시에서는 한 번에 하나의 쓰레드만이
incrementCounter
메서드에 접근할 수 있으므로 중복 실행은 발생하지 않는다.쓰레드가 동기화 메서드를 호출하면 lock을 얻으며, 실행을 완료하면 반납한다. 다른 쓰레드는 다시 lock을 얻어서 액세스 권한을 얻을 수 있다.
Synchronized Statements
메서드 전체를 synchronized 하는 것은 수행을 불필요하게 직렬화할 수 있으므로 일부만을 thread-safe하게 구현할 수 있다.
public void incrementCounter() { // additional unsynced operations synchronized(this) { counter += 1; } }
synchronized statements를 이용해서
incrementCounter
메서드를 구현하면 내부에서 동기화가 필요한 부분과 필요하지 않은 부분을 분리할 수 있다.단,
this
를 이용해서 lock을 제공하는 객체를 지정해야 한다.Read/Write Locks
Thread-safe를 구현하기 위한 강력한 방법 중 하나는
ReadWriteLock
을 사용하는 것 이다.읽기 전용 작업과 쓰기 전용 작업에 대한 Lock pair를 제공해서, 상황에 따른 접근 권한을 분리할 수 있다.
리소스에 쓰는 쓰레드가 없으면 여러 쓰레드가 리소스의 읽기 권한을 가질 수 있고, 있으면 다른 쓰레드는 읽기 권한을 가질 수 없다.
public class ReentrantReadWriteLockCounter { private int counter; private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); private final Lock readLock = rwLock.readLock(); private final Lock writeLock = rwLock.writeLock(); public void incrementCounter() { writeLock.lock(); try { counter += 1; } finally { writeLock.unlock(); } } public int getCounter() { readLock.lock(); try { return counter; } finally { readLock.unlock(); } } // standard constructors }
정리
Multi-Thread 환경에서는 자원의 동시 접근으로 인한 문제가 발생할 수 있다.
Thread-safe를 적용하기 위한 대상은 Field(Collection, Object), Method, Statements가 있다.
synchronized keyword를 사용하는 것은 강력한 thread-safe를 지원하지만 수행을 직렬화하기 때문에 동시성의 장점이 떨어진다.'Programming > Java' 카테고리의 다른 글
StackOverflowError가 뭘까? (사이트 아님) (421) 2021.11.08 POJO(Plain Old Java Object)와 JavaBean (423) 2021.10.08 Java에서 Thread를 사용하는 방법 (445) 2021.10.04 String Pool - Java의 String은 어디에 저장될까 (433) 2021.08.26 Checked and Unchecked Exception (422) 2021.08.25