개발자로서 살아남기

JAVA - Synchronized와 ThreadLocal 을 이용한 동시성 문제 해결

코드 살인마 2024. 5. 3. 21:52
728x90

개요

staticsynchronized의 연관관계에 대해 한번 정리를 해보려고 한다.

 

일단 JAVA에서 static은 클래스가 로딩될 때 메모리의 정적 영역(Static Area)에 할당된다.


이 영역은 특징은 다음과 같다.

  • JVM이 시작될 때 생성되며, 프로그램의 실행이 끝날 때까지 유지된다.
  • 클래스의 모든 인스턴스가 공유하는 공통된 값은 가진다.

즉 멀티스레딩 환경에서 주의하여 사용해야한다.

테스트

아래 예시는 주의하지 않고 사용한 코드이다.

public class A {  
    public static int a;  

    public static void plus() {  
       a++;  
    }  

}
public class ATest {  

    @Test  
    public void testPlusMethod() {  
       Runnable task = () -> {  
          for (int i = 0; i < 100; i++) {  
             A.plus();  
          }  
       };  

       Thread[] threads = new Thread[10];  
       for (int i = 0; i < threads.length; i++) {  
          threads[i] = new Thread(task);  
          threads[i].start();  
       }  

       for (Thread thread : threads) {  
          try {  
             thread.join();  
          } catch (InterruptedException e) {  
             e.printStackTrace();  
          }  
       }  ``

       // 검증  
       assertEquals(1000, A.a);  
    }  

}

 

위 코드의 기대 결과는 1000이지만 (100(반복횟수) * 10(스레드 개수)) 실제 결과값은 1000이 안나온다,

 

원인은 공유자원(static)에 여러 스레드가 접근하여, plus 메소드를 호출하기 때문에 발생한다.

 

해결법은 간단하다. synchronized 을 이용하면 된다.

public class A {  
    public static int a;  

    public synchronized static void plus() {  
       a++;  
    }  

}

 

 

테스트도 무난히 통과한다.

 

인스턴스 변수도 static과 마찬가지로 동시성 이슈가 발생한다. 특히 인스턴스를 1개만 사용하는 싱글톤 패턴에서 많이 발생한다.

 

지역변수만이 동시성 이슈로부터 자유로운데, 그 이유는 지역변수는 쓰레드 마다 갖고있는 메모리 공간(Stack 영역)에 저장되기 때문이다.

 

그렇다면, 객체의 필드를 사용하면서, 동시성 이슈를 해결할 수 있는 방법은 없을까?

 

쓰레드 로컬이 있다.

쓰레드 로컬

해당 쓰레드마다 접근할 수 있는 특별한 저장소이다. 즉 쓰레드마다 저장공간이 할당되어 있는 것이다.

 

아래 예제는 쓰레드 로컬을 사용한 코드이다.

public class A {  
    public ThreadLocal<Integer> a = new ThreadLocal<>();  

    public void set() {  
       a.set(0);  
    }  

    public int get() {  
       return a.get().intValue();  
    }  

    public void plus() {  
       int nA = a.get()+1;  
       a.set(nA);  
    }  

}

 

쓰레드 로컬을 사용하기 위한 메소드들을 생성하였다. 시작할 때, 0으로 세팅 후, +1 씩 100번 돌린다.

 

각 쓰레드마다 메모리가 할당되었으니. 예상 값은 100이다.

A a = new A();  

@Test  
public void testPlusMethod() {  
    Runnable task = () -> {  
       a.set();  
       for (int i = 0; i < 100; i++) {  
          a.plus();  
       }  
       assertEquals(100,a.get());  
    };

 

task 시작전 setplus 하고, 예상값 100인지 확인한다.

 

위코드는 정상작동한다!

 

값 저장 : ThreadLocal.set()
값 조회 : ThreadLocal.get()
값 제거 : ThreadLocal.remove()

 

주의해야할 점이 있다.

  • 쓰레드 로컬을 사용하고 나면 반드시 해당 쓰레드가 가지고 있는 메모리를 ThreadLocal.remove()로 제거해줘야한다. 제거하지 않으면, 메모리 누수 가능성이 있다.