[Java] 멀티 쓰레드 동시성 이슈

무1민·2023년 11월 10일
0

java

목록 보기
3/4
post-thumbnail

지난 시간에 자바로 멀티쓰레드를 구현하는 방법에 대해서 알아보았다.
이번엔 동시성 이슈를 처리하는 방법을 알아보겠다.

🧙동시성?

  • 동시에 실행되는 것처럼 보이는 것
  • 싱글 코어에서 멀티 쓰레드를 동작시키기 위한 방식으로, 멀티 태스킹을 위해 여러 개의 쓰레드가 번갈아 가면서 실행되는 성질을 말한다.

🎶동시성 이슈란?

멀티쓰레드 방식은 멀티 태스킹을 하는 방식 중, 한 코어에서 여러 쓰레드를 이용해 번갈아 작업을 처리하는 방식이다.
멀티 쓰레드를 이용하면 공유하는 영역이 많아 프로세스 방식보다 context switching 오버헤드가 작아, 메모리 리소스가 상대적으로 적다는 장점이 있다.
하지만, 쓰레드는 같은 자원을 공유하여 사용할 수 있기 때문에, 이 과정에서 동시성 이슈가 발생한다.
하나의 자원을 두고 경쟁상태같은 문제가 발생한다.

다음 코드를 확인하자

public class Count{
    private int count = 0;

    public void increase() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

간단해 보이는 이 코드는 thread-safe하지 않다.
쓰레드 A와 쓰레드 B가 동시에 increase()메서드를 호출하여 count를 증가시키면, A의 increase()로 count가 1이 되고, B의 increase()로 count가 2가 되어야 할 것이다.
하지만, A와 B가 동시에 increase() 메서드를 호출하면, count는 1이 되는 문제가 발생한다.

👾Thread safe란?

여러 쓰레드가 작동하는 환경에서도 문제 없이 동작하는 것을 Thread safe하다고 말한다.
즉, 동시성 이슈를 해결하고 일어나지 않는다면 Thread safe하다고 한다.

동시성을 제어하는 방법

1) 암시적 Lock (synchronized)

  • 기본적으로 Lock을 적용하게 되면 하나의 쓰레드가 해당 메서드를 실행하고 있을 때 다른 메서드가 해당 메서드는 실행하지 못하고 대기하게 된다.
  • lock은 메서드, 변수에 각각 걸 수 있고, 해당 메서드, 변수에 진입하는 쓰레드는 단 하나만 가능하다.
  • 변수에 lock을 걸기 위해선 해당 변수는 객체여야한다. int, long과 같은 기본형 타입에는 lock을 걸 수 없다.
  • 단점 : 한 번에 하나의 쓰레드만 메서드를 실행시킬 수 있으므로 병렬성은 매우 낮아진다.

동시성을 해결하는 데 가장 간단하면서 쉬운 방법은 Lock을 거는 것이다.
문제의 메서드, 변수에 각각 synchronized라는 키워드를 넣는 것이다.

public class Count {
    private int  count = 0;

    public synchronized void increase() { //synchronized 추가
        count++;
    }

    public synchronized int getCount() { //synchronized 추가
        return count;
    }
}

2) 명시적 Lock

synchronized 키워드를 사용하는 암시적 Lock과는 달리 Lock 인터페이스를 이용한다. Lock객체의 lock() 메서드를 호출하여 잠그고, unlock() 메서드를 호출하여 잠금을 해제한다.

public class Count {
    private int  count = 0;
    private Lock lock = new ReentrantLock();

    public void increase() {
        lock.lock(); //잠금
        try {
            count++;
        } finally {
            lock.unlock(); //잠금 해제
        }

    }

    public int getCount() {
        lock.lock(); //잠금
        try{
            return count;
        }finally{
            lock.unlock(); //잠금 해제
        }
    }
}

이러면 무조건 synchronized 쓰면 되잖아? 라고 생각했는데, Lock의 범위를 메서드 내부에서 한정하기 어렵거나, 동시에 여러 Lock을 사용하고 싶을 때 사용한다고 한다.

3) volatile

volatile 키워드를 사용하면 변수가 항상 메인 메모리에서 읽고 쓰이도록 보장된다.

public class Count {
    private volatile int count; //volatile 추가

    public void increase() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

캐시에 저장되지 않고 메인 메모리에 항상 저장이 되기 때문에, 캐시 사용으로 인한 데이터 불일치를 막을 수 있다.

4) Concurrent 패키지 사용

대표적으로 ConcurrentHashMap 클래스를 사용한다.

public class Count {
    private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

		public void increase() {
		    Integer currentValue = map.get("count");
		    if (currentValue == null) {//"count"라는 키가 Map에 존재하지 않으면
		        map.put("count", 1);
		    } else { //존재하면
		        map.put("count", currentValue + 1);
		    }
		}

    public int getCount() {
        return map.get("count");
    }
}

5) 불변 객체 사용

불변 객체는 상태가 변경될 수 없는 객체이기 때문에 Thread-safe하다.

public final class Count {
    private final int count;

    public Count(int count) {
        this.count = count;
    }
    public Count increase(){
        return new Count(count + 1); //새로운 Count 객체 생성
    }
    public int getCount(){
        return count;
    }
}

하지만, 불변 객체를 사용하면, 객체를 생성하는 비용이 굉장히 크기 때문에, 성능상 문제가 발생할 수 있다. 그렇기 때문에, 불변 객체를 재사용하는 방법을 이용한다.

profile
야호

0개의 댓글