안녕하세요. 개발자 Jindory입니다.
오늘은 Thread Safty하게 개발하는 방법에 대해서 글을 작성해보려고 합니다.
[ 글 작성 이유 ]
Java는 Muti-Thread 환경에서 실행되기 때문에, Thread Safety한 개발을 하지 않을 경우 예상치 못한 결과 초래될 수 있음을 인지하여, Thread Safety하게 개발하는 방법에 대해서 정리해보고자 글을 작성하게 되었습니다.
Thread Safty란?
우리는 Java 프로그램을 실행하면서 Mult-Thread 환경에서 실행할 수 있습니다. 이를 통해서 여러 스레드가 동시에 작업을 하면서, 절차지향적으로 실행될 때 보다 몇배의 성능으로 프로그램을 실행할 수 있습니다. 하지만 여러 스레드가 동일한 데이터에 대해서 접근하고 처리하는 과정에서 데이터의 값이 변경되거나 가공되면서 예상치 못한 결과가 나올 위험성이 있어 Multi-Thread 환경에서 개발할 경우 이 점을 유의하며, 개발해야합니다.
이렇게 여러개의 스레드를 가지고 프로그램을 실행하더라고 일관성있는 결과가 나오도록 개발하는 것을 Thread safty한 개발이라고 합니다.
Thread Safty하게 개발하는 방법
Thread Safty하게 개발하는 방법은 여러가지 방법이 있으므로 어떤 방법으로 Thread Safty한 상태를 유지할 수 있는지 차근차근 확인해보도록 하겠습니다.
1. Synchronized Statements
Synchronization는 한번에 하나의 스레드만 특정 작업을 완료하도록 허용하는 프로세스입니다. 이는 여러 스레드가 동시에 실행되고 동시에 동일한 리소스에 액세스하려고 하면 불일치 문제가 발생한다는 것을 의미합니다. 따라서 Synchronized는 한 번에 하나의 스레드만 허용하여 불일치 문제를 해결하는 데 사용됩니다.
Synchronization은 synchoronized라는 키워드를 사용하며, Synchronized는 임계영역(Critical section)이라고 알려진 코드 블록을 생성합니다. 임계 영역은 한번에 하나의 스레드만 실행되도록 보장하며, 다른 스레드가 동시에 동일한 객체에 접근하지 못하도록 합니다.
아래의 코드는 매개변수로 받은 n이라는 int값에 1부터 5를 더하면수 출력하는 소스입니다. sum이라는 method에 synchronized 키워드를 통해 여러개의 스레드(A,B)가 동시에 실행되더라도 임계영역(Critical section)으로 블록을 생성하여 하나의 Thread가 접근시 다른 Thread를 접근하지 못 하게 하므로 하나의 Thread가 실행한 후 다른 Thread가 sum method를 실행할 것입니다.
class A {
synchronized void sum(int n){
Thread t = Thread.currentThread();
for(int i=1; i<=5; i++){
System.out.println(t.getName() + " : "+(n+i));
}
}
}
class B extends Thread{
A a = new A();
public void run(){
a.sum(10);
}
}
public class Test {
public static void main(String[] args) {
B b = new B();
Thread t1 = new Thread(b);
Thread t2 = new Thread(b);
t1.setName("Thread A");
t2.setName("Thread B");
t1.start();
t2.start();
}
}
아래의 결과는 실제로 출력된 결과물이며, synchronized 키워드를 사용했을때와 사용하지 않았을때를 비교하면, synchronized 키워드 사용시 하나의 Thread가 method 실행을 다 끝낸 후 다음 Thread의 결과가 나옴을 알 수 있습니다.
-------------------- synchronized keyword 사용 --------------------
Thread A : 11
Thread A : 12
Thread A : 13
Thread A : 14
Thread A : 15
Thread B : 11
Thread B : 12
Thread B : 13
Thread B : 14
Thread B : 15
-------------------- synchronized keyword 미사용 ------------------
Thread A : 11
Thread A : 12
Thread B : 11
Thread A : 13
Thread B : 12
Thread A : 14
Thread A : 15
Thread B : 13
Thread B : 14
Thread B : 15
2. ReentrantLock
Thread를 동기화 할 수 있는 방법은 synchronized 블럭 외에도 'java.util.concurrent.locks' 패키지가 제공하는 lock클래스들을 이용하는 방법이 있습니다. 이 패키지는 JDK 1.5에 와서야 추가된 것으로 그전에 동기화 방법은 synchronized 블럭뿐이었습니다.
synchronized블록으로 동기화를 하면 자동으로 lock이 잠기고 풀리기 때문에 편리합니다. 심지어 synchronized블럭 내에서 예외가 발생해도 lock는 자동적으로 풀립니다. 그러나 때로는 같은 메서드 내에서만 lock를 걸 수 있다는 제약이 불편하기도 합니다. 그럴 때 이 lock 클래스를 사용하면 됩니다.
lock 클래스의 종류는 다음과 같이 3개가 있습니다.
- ReentratLock : 재진입이 가능한 lock. 가장 일반적인 배타 lock
- ReentratReadWriteLock : 읽기에는 공유적이고, 쓰기에는 배타적인 lock
- StampedLock : ReentratReadWriteLock에 낙관적인 lock의 기능을 추가
ReentrantLock
ReentrantLock 인스턴스를 사용하면 대기중인 Thread가 일부 유형의 리소스 부족을 겪는 것을 방지하여 정확하게 이를 수행할 수 있습니다.
ReentrantLock 생성자 아무런 매개변수가 없는 생성자와 선택적 fair boolean을 넘겨주는 두가지 생성자가 있습니다.
ReentrantLock()
ReentrantLock(boolean fair)
true로 설정되면 여러 Thread가 잠금을 획득하려고 시도하는 경우 JVM은 가장 오래 대기하는 Thread에 우선순의를 부여하고 잠금에 대한 액세스 권한을 부여합니다. 그러나 공정하게 처리하려면 어떤 Thread가 가장 오래 기다렸는지 확인하는 과정을 거칠 수 밖에 없으므로 성능은 떨어지게 됩니다.
ReentratReadWriteLock
ReentrantReadWriteLock 또한 기본 생성자와 대기한 Thread를에게 먼저 권한을 주는 공정모드 생성자가 있습니다.
ReentrantReadWriteLock()
ReentrantReadWriteLock(boolean fair)
ReentrantLock와의 차이점은 읽기 위한 lock과 write를 위한 lock을 제공한다는 것입니다. 또한 읽기 lock이 걸려 있더라도, 다른 Thread가 읽기 lock을 중복해서 걸고 읽어도 문제가 되지 않습니다. 그러나 읽기 lock이 걸린 상태에서 쓰기 lock을 거는것은 허용되지 않습니다. 반대로 쓰기 lock이 걸린 상태에서 읽기 lock을 동시에 얻는것이 허용되지 않습니다.
StampedLock
StampledLock은 lock을 걸거나 해지할 때 스탬프(long타입의 정수값)를 사용하며, 읽기와 쓰기를 위한 lock외에 낙관적 읽기 lock(Optimistic reading lock)이 추가된 것입니다. 읽기 lock이 걸려 있으면, 쓰기 lock을 얻기 위해서는 읽기 lock이 풀릴 때까지 기다려야 하는데 비해 '낙관적 읽기 lock은 쓰기 lock에 의해 바로 풀립니다. 그래서 Optimistic reading lock에 실패하면, 읽기 lock을 얻어서 다시 읽어와야 합니다. 무조건 읽기 lock을 걸지 않고, 쓰기와 읽기가 충돌할 때만 쓰기가 끝난 후에 읽기 lock을 거는 것입니다.
ReentrantLock 사용방법
public class ReentrantLockCounter {
private int counter;
private final ReentrantLock reLock = new ReentrantLock(true);
public void incrementCounter() {
reLock.lock();
try {
counter += 1;
} finally {
reLock.unlock();
}
}
// standard constructors / getter
}
위 코드를 보게되면 lock 메서드가 시작된 지점부터 unlock메서드가 실행되는 시점까지를 임계 영역으로 설정한다고 보면 될것 같습니다. 자동적으로 lock의 잠금과 해제가 관리되는 synchronized 블럭과 달리, ReentrantLock과 같은 lock 클래스들은 수동으로 lock을 잠그고 해제해야하기 때문에 lock을 걸고 나서 푸는것을 잊어버리는 실수를 하지 않도록 주의를 기울여야 합니다.
아래의 코드와 같이 lock을 특정 조건에 맞는 Thread에게만 notify 하거나 특정 Thread에를 대기할 수 있도록 구분하여 await과 signal을 할 수 있습니다.
private ReentrantLock lock = new ReentratLock(); // lock을 생성
private Condition forCosumer = lock.newCondition(); // 소비자를 위한 lock condition
private Condition forProducer = lock.newCondition(); // 생산자를 위한 lock condition
아래의 코드는 소비자와 생산자가 같은 자원을 가지고 경쟁하는 경쟁상태와 없는 자원을 기다리는 상태로 빠지는 기아 현상을 방지하기 위해 각각의 lock Condition에 맞춰 조건에 맞는 대기와 활성화하는 방법을 일부 구현한 코드입니다.
package thread.example;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class GoodsManager {
public static void main(String[] args) throws Exception {
Store store = new Store();
new Thread(new Producer(store),"Producer1").start();
new Thread(new Consumer(store,"iphone"),"Consumer1").start();
new Thread(new Consumer(store,"galaxy"),"Consumer2").start();
new Thread(new Consumer(store,"xiaomi"),"Consumer3").start();
}
}
class Consumer implements Runnable{
private Store store;
private String goodsName;
Consumer(Store store,String goodsName){
this.store = store;
this.goodsName = goodsName;
}
@Override
public void run() {
// 가게에서 제품을 판매중이라면, 일단 가게 입장
while(store.isForSale){
try{Thread.sleep(1000);}catch(InterruptedException e){}
String name = Thread.currentThread().getName();
// 가게에서 상품 구매
store.sellGoods(goodsName);
System.out.println(name+" buy product "+goodsName);
System.out.println("Remain Goods : "+store.getGoodsBox().toString());
}
System.exit(0);
}
}
class Producer implements Runnable{
private Store store;
Producer(Store store){this.store = store;}
public void run(){
// 누적 판매량이 최대 판매량을 초과하지 않을 경우 계속 생산.
while(store.cumSalesVol<store.MAX_SALES_GOODS) {
int idx = (int) (Math.random() * store.goodsNum());
store.produceGoods(store.goodsNames[idx]);
try { Thread.sleep(10); } catch (InterruptedException e) { }
}
Thread.interrupted();
}
}
class Store {
String[] goodsNames = {"iphone","galaxy","xiaomi"};
int cumSalesVol = 0;
final int MAX_SALES_GOODS = 30;
final int MAX_GOODS = 10;
private List<String> goodsBox = Collections.synchronizedList(new ArrayList<>());
boolean isForSale = true;
public List<String> getGoodsBox(){
return this.goodsBox;
}
private ReentrantLock lock = new ReentrantLock();
private Condition forConsumer = lock.newCondition(); // 소비자를 위한 lock condition
private Condition forProducer = lock.newCondition(); // 생산자를 위한 lock condition
public void produceGoods(String goodsName){
lock.lock();
try{
while(goodsBox.size()>=MAX_GOODS){
String name = Thread.currentThread().getName();
System.out.println(name+" is waiting");
try{
forProducer.await();
Thread.sleep(500);
}catch(InterruptedException e){}
}
goodsBox.add(goodsName);
forConsumer.signal();
System.out.println("Goods Box : "+goodsBox.toString());
}finally {
lock.unlock();
}
}
public void sellGoods(String goodsName){
lock.lock();
String name = Thread.currentThread().getName();
try{
// 상품 박스에 상품이 없다면
while(goodsBox.size()==0){
// 만일 누적 판매수를 달성했다면, 더이상 물품이 생산되지 않을 예정이므로 isForSale을 false로 바뀐다.
manageDoorOpenedClosed();
if(isForSale){
System.out.println(name+" is waiting");
try{
forConsumer.await();
Thread.sleep(500);
}catch(InterruptedException e){}
}else{
// 가게가 문을 닫았다면, 가게를 나간다.
System.out.println(name+" exit store");
Thread.interrupted();
}
}
while(isForSale){
for(int i=goodsBox.size()-1;i>=0;i--){
if(goodsName.equals(goodsBox.get(i))){
goodsBox.remove(i);
cumSalesVol++;
System.out.println("Cumulative sales volume : "+cumSalesVol);
manageDoorOpenedClosed();
forProducer.signal();
return;
}
}
try{
// GoodsBox에 상품이 진열되어 있다면, 일단 구매하러 입장
if(goodsBox.size()>0){
// 판매중이라면 일단 대기
if(isForSale){
System.out.println(name + " is waiting.");
forConsumer.await();
Thread.sleep(500);
}else{
System.out.println(name+" exit store");
Thread.interrupted();
}
}
}catch(InterruptedException e){}
}
}finally {
lock.unlock();
}
}
public int goodsNum() {return goodsNames.length; }
public void manageDoorOpenedClosed(){
// 목표 누적 판매량 초과달성 및 재고 소진시 문을 닫는다.
if(cumSalesVol>MAX_SALES_GOODS && goodsBox.size()==0){
isForSale = false;
}
}
}
위 코드와 같이 상품이 부족할때는 소비자는 대기하고, 생산자에게는 알리고, 상품이 최대 생산량을 초과할 경우 생산자는 기다리고 소비자에게 알려서 기아 현상과 경쟁상태를 개선하는 것을 알 수 있습니다.
3.Stateless Implement(무상태 구현)
객체를 무상태(Stateless)로 구현하는 방법입니다. Stateless하게 객체를 구현한다는 의미는 상태를 가지지 않는다 즉 "인스턴스 변수(필드)"를 가지지 않는 경우를 말합니다. 여기서 말하는 "State(상태)"는 객체의 속성이나 데이터 값을 의미합니다. 예를 들어 계산기 객체가 있다고 가정할 때, 계산기는 입력된 숫자를 더하거나 빼고,곱하고,나누는 등의 연산을 수행하는데, 이 때 계산 결과를 객체 내부에 저장하지 않고 매번 메서드 호출시에 전달받은 숫자만 계산합니다. 이렇게 상태를 저장하지 않기 때문에 여러개의 Thread가 동시에 접근한다고 하더라도 공유되는 자원이 없기 때문에 자원 공유로 인한 변화가 생기지 않으며, 항상 일정한 결과를 도출할 수 있는것입니다.
아래의 코드는 Stateless하게 구현된 factorial을 구하는 MathUtils 객체입니다.
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 메서드에서 사용하는 값들은 매개변수와 method안 지역 변수들 뿐입니다. 매개변수와 지역 변수는 각각의 Thread에 따로 관리되기 때문에 다른 Thread가 간섭할 수 없어서 항상 일정한 factorial 결과를 도출할 수 있는것입니다.
4. Immutable Implement(불변 구현)
만일 서로 다른 Thread가 상태를 공유해야하는 경우, 멤버변수를 불변으로 만들어서 다수의 스레드로부터 안전하게 만들어야 합니다. 멤버변수를 불변 객체 혹은 불변 타입으로 만들면, 한번 선언한 이후에 데이터가 바뀌더라도 새로운 객체를 만드므로 데이터의 안정성이 올라갑니다. 또한 setter method를 만들지 않음으로써 외부에서 데이터를 쉽게 변경할 수 없도록 구현해야 합니다.
public class MessageService {
private final String message;
public MessageService(String message) {
this.message = message;
}
// standard getter
public String getMessage(){
return this.message;
}
}
위 코드는 멤버변수에 final 키워드를 붙혀서 한번 초기화 한 데이터를 바꿀 수 없게 하고, 생성자를 통해서만 멤버변수의 값이 할당되도록 구현되어 있습니다. 또한 getter 메서드는 구현하고 setter를 구현하지 않음으로써 외부에서 MessageService의 멤버변수에 접근하지 못 하도록 만듬으로써, Thread-Safty한 코드를 구현했습니다.
5. Thread-Local Field(Thread 지역 변수)
실제로 Thread의 상태를 유지해야하는 경우, 필드를 Thread Local로 만들어 Thread 간에 상태를 공유하지 않는 스레드로부터 안전한 클래스를 만들 수 있습니다. Thread Class에서 filed를 private로 정의하면 다른 스레드에서 접근 할 수 없는 상태를 만들 수 있습니다.
아래의 2개의 코드는 각각의 Thread Class에 Local filed를 만든 Thread Class로 ThreadA와 ThreadB에서 선언한 Local filed에 대해서 다른 Thread가 접근할 수 없습니다.
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);
}
}
public class ThreadB extends Thread {
private final List<String> letters = Arrays.asList("a", "b", "c", "d", "e", "f");
@Override
public void run() {
letters.forEach(System.out::println);
}
}
이러한 구현을 통해 각각의 Thread는 상태 filed를 가지면서, 다른 스레드로부터 간섭받지 않는 Thread-Safe한 구현이 가능합니다.
6. Synchronized Collections(동기화 Collection)
우리는 이미 구현된 Collection Framework 중에 Synchronize Wrapper set를 사용하여 스레드로부터 안전한 Collection을 쉽게 만들 수 있습니다. Synchronized Collection을 구현하면 다중 스레드 환경에서 여러 스레드가 동시에 접근해도 데이터 무결성을 보장하며, 동시성 문제를 방지합니다.
Synchronized Collection의 종류는 아래와 같습니다.
- synchronizedCollection() 메서드
- Collections.synchronizedCollection(Collection<T> e)를 사용하여 객체를 생성합니다.
- 지정된 Collection을 기반으로 하는 스레드 안전한 컬렉션을 반환합니다.
- 생성 방법 : Collection<Integer> syncCollection = Collections.synchronizedCollection(new ArrayList<>));
- synchronizedList() 메서드
- Collections.synchronizedList(List list)를 사용하여 객체를 생성합니다.
- 지정된 List를 기반으로 하는 스레드 안전한 List를 반환합니다.
- 생성 방법 : List syncList = Collections.synchronizedList(new ArrayList<>());
- synchronizedSet() 메서드
- Collections.synchronizedSet(Set s)를 사용하여 객체를 생성합니다.
- 지정된 Set 기반으로 하는 스레드 안전한 Set을 반환합니다.
- 생성방법 : Set syncSet = Collections.synchronizedSet(new Set<>());
- synchronizedMap() 메서드
- Collections.synchronizedMap(Map<K,V> m)을 사용하여 객체를 생성합니다.
- 지정된 Map을 기반으로 스레드 안전한 Map을 반환합니다.
- 생성 방법 : Map<String,Integer> syncMap = Collections.synchronizedMap(Map<>());
동기화된 Collection은 각 메서드에서 고유 잠금을 사용한다는 점을 명심해야합니다.
아래는 Collection 객체 안에 정의된 SyncrhonizedCollection 객체 및 메서드와 관련된 코드입니다.
// Collection 내에 정의된 SynchronizedCollection
static class SynchronizedCollection<E> implements Collection<E>, Serializable {
private static final long serialVersionUID = 3053995032091335093L;
final Collection<E> c; // Backing Collection
final Object mutex; // Object on which to synchronize
SynchronizedCollection(Collection<E> c) {
this.c = Objects.requireNonNull(c);
mutex = this;
}
SynchronizedCollection(Collection<E> c, Object mutex) {
this.c = Objects.requireNonNull(c);
this.mutex = Objects.requireNonNull(mutex);
}
public int size() {
synchronized (mutex) {return c.size();}
}
public boolean isEmpty() {
synchronized (mutex) {return c.isEmpty();}
}
public boolean contains(Object o) {
synchronized (mutex) {return c.contains(o);}
}
public Object[] toArray() {
synchronized (mutex) {return c.toArray();}
}
public <T> T[] toArray(T[] a) {
synchronized (mutex) {return c.toArray(a);}
}
public Iterator<E> iterator() {
return c.iterator(); // Must be manually synched by user!
}
public boolean add(E e) {
synchronized (mutex) {return c.add(e);}
}
public boolean remove(Object o) {
synchronized (mutex) {return c.remove(o);}
}
public boolean containsAll(Collection<?> coll) {
synchronized (mutex) {return c.containsAll(coll);}
}
public boolean addAll(Collection<? extends E> coll) {
synchronized (mutex) {return c.addAll(coll);}
}
public boolean removeAll(Collection<?> coll) {
synchronized (mutex) {return c.removeAll(coll);}
}
public boolean retainAll(Collection<?> coll) {
synchronized (mutex) {return c.retainAll(coll);}
}
public void clear() {
synchronized (mutex) {c.clear();}
}
public String toString() {
synchronized (mutex) {return c.toString();}
}
// Override default methods in Collection
@Override
public void forEach(Consumer<? super E> consumer) {
synchronized (mutex) {c.forEach(consumer);}
}
@Override
public boolean removeIf(Predicate<? super E> filter) {
synchronized (mutex) {return c.removeIf(filter);}
}
@Override
public Spliterator<E> spliterator() {
return c.spliterator(); // Must be manually synched by user!
}
@Override
public Stream<E> stream() {
return c.stream(); // Must be manually synched by user!
}
@Override
public Stream<E> parallelStream() {
return c.parallelStream(); // Must be manually synched by user!
}
private void writeObject(ObjectOutputStream s) throws IOException {
synchronized (mutex) {s.defaultWriteObject();}
}
}
// Synchronized Collection 생성 메서드
public static <T> Collection<T> synchronizedCollection(Collection<T> c) {
return new SynchronizedCollection<>(c);
}
따라서 Synchronized와 관련된 메서드 사용시 동기화된 액세스로 인해 기본 성능이 저하되는 패널티는 가지고 있다는 점을 인지하고 사용해야합니다.
7. 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");
Synchronized Collection과 달리 Concurrent Collection은 데이터를 세그먼트로 나누어 Thread의 안전성을 달성합니다.
예를 들어 ConcurrentHashMap에서는 여러 스레드가 서로 다른 맵 세그먼트에 대한 잠금을 획득 할 수 있으므로 여러 스레드가 동시에 맵에 액세스 할 수 있습니다. 아래와 같이 배열을 생성하고 각각의 배열의 인덱스는 HashMap을 나타냅니다.(JDK 1.8 이전의 구조)
ConcurrentHashMap 구조
ConcurrentHashMap이 Thread safe하게 데이터를 관리하며 효율을 유지하는 과정
동기화된 Concurrent Collection은 콘텐츠가 아닌 Collection 자체를 스레드로부터 안전하게 만들 뿐이라는 점을 언급할 가치가 있습니다. 그래서 Concurrent Thread access의 고유한 장점으로 인해 Concurrent Collection은 Synchronized Collection 보다 성능이 훨씬 더 좋습니다.(ConcurrentHashMap의 성능에 관한 내용은 여기를 참조하시면 더 많은 정보를 얻으실 수 있습니다.)
8. Atomic Object
AtomicInteger, AtomicLong, AtomicBoolean 및 AtomicReference를 포함하여 Java가 제공하는 원자 클래스 세트를 사용하여 Thread Safe를 달성하는것도 가능합니다.
Atomic 클래스를 사용하면 동기화를 사용하지 않고도 Thread로부터 안전한 원자 작업을 수행할 수 있습니다. Atomic 작업은 단일 기계 수준 작업에서 실행됩니다.
원자 Class에 있는 원자라는 말에 대해서 쉽게 설명하자면, 원자성이랑 더이상 쉽게 쪼개질 수 없는 성질이라는 의미로 특정 작업 및 물질을 쪼갤 수 없는 단위로 만드는 것입니다. 프로그램 코드로 설명하자면 아래와 같이 I++; 이라는 작업을 수행하기 위해서는 오르쪽 그림과 같은 작업이 수행되어야 합니다. 만일 이 작업들 사이에 Thread가 간섭하여 i의 값을 이전 값으로 조회하거나 다른 값으로 변경한다면, 원치 않는 결과가 나오게 될것입니다.
그래서 여러개로 나눠진 절차를 하나의 원자로 만듬으로써 더이상 쪼개질 수 없는 단위로 만들고, 중간에 다른 작업이 끼어들지 못하게 하는것입니다.
Atomic Class에서 연산을 하기위해 거치는 절차는 아래와 같다고 합니다.
- 인자로 기존 값과 변경할 값을 전달한다.
- 기존값으로 던진 값이 현재 시스템이 가지고 있는 값과 같다면 변경할 값을 반영해준다. 반환 값으로 true 리턴한다.
- 반대로 기존 값으로 던진 값이 현재 시스템이 가지고 있는 값과 다르다면 값을 반영 하지 않고 false를 리턴한다.
실제로 횟수를 세는 Count라는 객체를 만들때 기존의 int로 구현한다면 아래와 같이 나옵니다.
public class Counter {
private int counter = 0;
public void incrementCounter() {
counter += 1;
}
public int getCounter() {
return counter;
}
}
이를 Atomic Class로 구현하면 아래와 같이 변경 할 수 있습니다.
public class AtomicCounter {
private final AtomicInteger counter = new AtomicInteger();
public void incrementCounter() {
counter.incrementAndGet();
}
public int getCounter() {
return counter.get();
}
}
위 코드를 보듯이 값을 증가시키는 method의 이름으로 알 수 있듯이 증가와 값을 가져오는 Operation을 한번에 한다는것을 유추할 수 있습니다.
// AtomicInteger의 incrementAndGet method
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// unsafe의 getAndAddInt method
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
이렇게 Thread Safty하게 개발하는 방법에 대해서 알아봤습니다.
혹시라도 정정할 내용이나 추가적으로 필요하신 정보가 있다면 댓글 남겨주시면 감사하겠습니다.
오늘도 Jindory 블로그에 방문해주셔서 감사합니다.
[ 참조 ]
Thread Safety and how to achieve it in Java - GeeksforGeeks
https://www.baeldung.com/java-thread-safety
https://velog.io/@mangoo/java-thread-safety
https://itsromiljain.medium.com/curious-case-of-concurrenthashmap-90249632d335
'개발 > Java' 카테고리의 다른 글
[Java] Generic의 컴파일 타임에 일어나는 Type Erasure (0) | 2024.01.09 |
---|---|
[Java] ArrayList 길이가 가변적으로 확장되는 방식(JDK 별) (2) | 2024.01.01 |
[Java] Java Garbage Collection 동작 과정 (0) | 2023.12.16 |
[Java] Java에서 Thread Unsafe한 상황 이해하기 (0) | 2023.12.15 |
[Java] Mutable Object와 Immutable Object (2) | 2023.12.15 |