[JAVA] 멀티스레드 동기화 해결기

yeonjoo913·2024년 3월 4일

JAVA

목록 보기
19/19

싱글스레드 프로세스의 경우 프로세스 내에서 단 하나의 스레드만 작업하기 때문에 별 문제가 없지만, 멀티스레드 프로세스의 경우 여러 스레드가 같은 프로세스 내의 자원을 공유해서 작업하기 때문에 서로 작업에 영향을 준다.

스레드 동기화란?

한 스레드가 진행중인 작업을 다른 스레드가 간섭하기 못하도록 막는 것을 스레드 동기화라고 한다. 멀티 스레드일 경우 두 개 이상의 스레드가 공유 데이터에 동시에 접근하게 되면 예상과 벗어난 결과가 나타날 수 있다.

  • 임계영역 : 프로세스간에 공유자원에 접근하는데에 있어 문제가 발생하지 않도록 한번에 하나의 프로세스만 이용하게 끔 보장하는 영역
  • 락 : 하나의 스레드나 프로세스가 자원을 사용하고 있는동안 잠금을 하여 접근 못하게 하는 방식

동기화 처리 하는 방법

  • Synchronized

    • synchronized 키워드를 사용하여 임계영역을 설정

      • 메소드 전체를 임계영역으로 지정하는 방법
      • 특정한 영역에만 선언하여 특정 영역만 임계영역으로 지정하는 방법
    • 메소드 전체를 지정하는 것 보다 특정 블럭을 지정하는 것이 성능에 좋다.

    • 싱글스레드일때는 괜찮으나 멀티스레드 일 경우 성능에 문제가 생길 수 있다.

      public void execute(String topic, Object payload) {
          makeThread();
          synchronized (queue) {
              try {
                  queue.addLast(new TopicData(topic, payload));
              } catch (Exception e) {
                  // Do nothing
              }
              queue.notifyAll();
          }
      }
  • Volatile(볼러타일)

    • 변수 앞에 volatile을 붙이면 코어가 변수의 값을 읽어올 때 캐시값을 읽어오는 것이 아니라 메모리에 있는 값을 읽어오기 때문에 캐시와 메모리 간의 값 불일치하는 문제를 해결할 수 있으면서 동기화를 해준다.
    • 오직 한 개의 쓰레드에서 쓰기 & 읽기 작업을 할 때, 그리고 다른 쓰레드들은 읽기 작업만 할 때 안정성을 보장(스레드의 접근을 제한하지 않고, 스스로 접근하지 않도록 해서 동기화 처리하는 것이 핵심)
  • Concurrent 패키지 사용

    • concurrentHashMap

      • 버킷 단위로 동기화를 처리. 이전 버전의 세그먼트 분할 방식이나 synchronized 처리와 비교해 최소한의 락 사용으로 스레드 안정성을 유지한다. (전체 Map에 대하여 락을 거는 것이 아니라, 필요한 부분만 table bucket에 대한 락을 사용하여 성능을 높임.)

      • synchronized 처럼 스레드의 접근을 제한하는 것이 아니라 volatile과 cas 연산을 활용해서 스레드를 동기화한다.

        	public static void main(String[] args) {
        			Map<String, Integer> concurrentHashMap = new ConcurrentHashMap<>();
        
        			concurrentHashMap.put("data1", 1);
        			concurrentHashMap.put("data2", 2);
        			concurrentHashMap.put("data3", 3);
        }	
    • copyOnWriteArrayList

      • set,add 과정에서 락과 함께 array를 copy후 write하는 방식으로 처리하기 때문에 get을 수행할 때 thread-safe하게 조회 가능하다.

      • 동기화된 arrayList를 생성할 수 있음.(synchronizedList보다 성능이 좋다)

      • read 작업량 < write 작업량 : SynchronizedList

      • read 작업량 > write 작업량 : CopyOnWriteList

      • 번외) 컨슈머에서 가져온 데이터들을 summary하여 쓸 때 SynchronizedList를 사용하여 처리하였음.

        private List<Report> reportList = Collections.synchronizedList(new ArrayList<>());
        
        		public void appendData (Data record, boolean retry){
        
        			Report report
                              = Report.builder()
                              .yyyymmdd(record.getYyyymmdd())
                              ...
                              .build();
        
                    if(retry){
                        reportRetryList.add(report);
                    } else {
                        reportList.add(report);
                    }
                }
        			```
        
    • Atomic

      • Wrapper class의 일종으로 참조 자료형과 기본 자료형 두 종류의 변수에 모두 적용가능하다. 사용시 내부적으로 CAS알고리즘을 사용해 락 없이 동기화 처리를 할 수 있다.

** CAS(Compare and Swap)
- 멀티스레드 환경에서 동시성 처리를 위한 방법 중 하나로, 특정 메모리 위치 값이 주어진 값과 동일한 경우 해당 메모리 주소를 새로운 값으로 교체한다. Atomic 연산으로 동시성 문제를 처리하는 Non-blocking 방식이다.

  • ThreadLocal
    • 스레드 별로 서로 다른 값을 처리해야할 필요가 있을 때 사용한다.
    • 스레드로컬에 저장된 값은 해당 스레드에서 고유하게 사용할 수 있다.
    • 스레드로컬 클래스의 변수는 private static final로 선언된다.
    • 사용이 끝난 후에는 remove()메소드를 호출해주는 습관을 들여야 한다.
      • web 기반의 application에서는 Thread를 재사용하기 위해 ThreadPool이라는 것을 사용한다. ThreadPool을 사용하면,Thread가 시작된 후 그냥 끝나는 것이 아니기 때문에 remove()를 사용해서 값을 지워줘야만 해당 Thread를 다음에 사용할 때 쓰레기 값이 들어있지 않게 된다.

멀티스레드는 공부할 때마다 헷갈리고 어렵지만,꾸준히 하다 보면 하나씩 이해하는게 재밌는 파트이다. 아직 부족한게 많지만 조금씩 공부하면서 정리해보겠다.

profile
주니어 백엔드 개발자. 까먹는다 기록하자!

0개의 댓글