본문 바로가기

Web Sever 개발과 CS 기초/자바

ThreadLocal, Atomic 이해와 CAS 원리

개요 목적

스레드를 사용하면 공유된 자원에 접근하여 예기치 못한 문제가 발생할 수 있다.

이번 시간에는 스레드 별로, 개별 공간을 제공하는 ThreadLocal과 Atomic에 대해서 알아본다.

마지막에는 Atomic을 가능하게 하는 CAS 동작 원리에 대해서도 알아본다.

ThreadLocal 

ThreadLocal은 스레드 별로, 자신만의 변수를 만들어준다.

ThreadLocal 변수 없이 일반 변수를 사용하면 모든 스레드가 공유된 자원을 사용하게 된다.

예시) 일반 변수 String a; thread1이 a에 값을 “t1”을 set한다(저장한다). thread2가 a의 값을 get(꺼내온다)하면 thread2가 설정하지 않은 “t1”의 값이 나온다. 공유된 자원을 사용하기 때문에 thread2가 설정하지 않은 "t1"의 값을 얻게 된다.

 

반면에 ThreadLocal로 지정한 변수는 thread1, thread2에게 독립적 변수 공간을 제공한다.

public class ThreadLocalTest implements Runnable {
	//핵심!!!
    //ThreadLocal 변수 지정하기 원하는 타입과 초기 값을 지정할 수 있다.
    private static final ThreadLocal<String> info= new ThreadLocal<>() {

        protected String initialValue() {
            return new String("Default");
        }
    };

    @Override
    public void run() {
        System.out.println("Strart Thred " +Thread.currentThread().getName() 
                             + " - value: " +info.get());
		info.set(Thread.currentThread().getName());
        System.out.println("End Thread" + Thread.currentThread().getName() 
                             +" - value: " +info.get());

    }

    public static void main(String[] args) {
        ThreadLocalTest runnable = new ThreadLocalTest();
        for (int i = 0; i < 3; i++) {
            Thread thread = new Thread(runnable);
            thread.start();
        }
    }
}

<결과>

Strart Thred Thread-2 - value: Default
Strart Thred Thread-1 - value: Default
End ThreadThread-1 - value: Thread-1
End ThreadThread-2 - value: Thread-2
Strart Thred Thread-0 - value: Default
End ThreadThread-0 - value: Thread-0

스레드 1,2,3별로 각자만의 String 값을 설정하고 불러 왔다.

Atomic Type

lock객체와 synchronized 블럭 사용 없이,  Atomic으로 설정된 변수는, 동기화를 지원한다.

= 여러 쓰레드가 하나의 변수에 동시에 접근해도 예상하지 못한 결과가 나오는 동시성 문제를 방지한다.

<Atomic Type 사용예제>

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicTest implements Runnable {

    public int num1 = 0;
    //핵심
    //일반 Integer가 아니라 Atomic으로 설정된 integer
    public AtomicInteger num2 = new AtomicInteger(0);

    @Override
    public void run() {
        for (int i = 0; i < 10000000; i++) {
            num1++;
            num2.incrementAndGet();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        LockTest runnable = new LockTest();

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        Thread t3 = new Thread(runnable);

        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();
        System.out.println("아토믹 사용 x ");
        System.out.println(runnable.num1);
        System.out.println("아토믹 사용 o");
        System.out.println(runnable.num2);
    }
}

<결과>

아토믹 사용 x 
28420461
아토믹 사용 o
30000000

Atomic과 CAS 원리 이해

Atomic이란, 원자 단위 연산의 결과를 보장해 주는 것이다.

먼저, CPU의 연산 과정을 보면서 원자 단위의 연산이 무엇인지 알아보자.

int a =1; 라는 변수에 +1을 하는 상황이다.

(메인메모리에서 a 변수를 CPU내 메모리 레지스터에 불러온다. → CPU에서 +1 더한다. → 레지스터에 +1한 값을 옮긴다. → 다시 레지스터에서 메인 메모리로 값을 옮겨 int a =2 값을 완성한다.)

(메인메모리 → 레지스터(cpu내메모리) → CPU연산 → 레지스터 → 메인메모리)

  • 레지스터란, CPU내에서 연산의 중간 결과나, 처리할 명령어들을 일시적으로 기억하는 임시 기억 장치이다.

 

Atomic이 필요한 상황

보통의 경우라면, 컴퓨터가 원자단위 연산을 실수할 리가 없다.

그렇다면, 원자단위 연산에서 오류가 나는 상황을 알아보자.

즉 Atomic 원자단위연산 결과값 보장이 필요한 상황이다. 

그것은 바로 멀티 스레드 상황이다.

두개의 스레드가 int a에 동시에 접근하여, +1를 동시에 진행한다면, 결과 값이 오류가 발생한다.

→ 동시에 int a = 2를 만들고(각자의 레지스터에서) 메인 메모리에 옮기는 과정에서 ++연산은 총 두번 일어 났지만, 결과 값은 int a = 2가 되는 오류가 발생한다. = (가시성문제)

이럴 때 Atomic으로 설정한 변수가 필요한 것이다.

→ Atomic으로 설정된 변수는, 여러 쓰레드가 하나의 공통 변수에 접근하여도, 원자 단위의 연산의 결과 값을 보장해주는 것이다.

Atomic을 가능하게 해주는 것이 CAS알고리즘이다.

CAS 알고리즘 원리와 과정을 예시로 파악해보자.

스레드1에서 메인 메모리에서 A 값을 가져온다. 
레지스터(CPU임시저장소)에 A를 저장한다.
CPU연산을 통해 결과 값 B를 만든다. 레지스터에 B를 저장한다.

메인메모리 A값을 연산된 B값으로 대체하기 전에 
메인메모리 A값과 레지스터 A에 값이 같은 지 확인한다.
같다면 B로 대체하여 연산을 마무리 한다.

레지스터 A값과 메모리 A값이 다르면, 연산을 다시 진행한다.
(메인 메모리에서 A값 가져오기 부터 시작)
값이 다른 이유 → 스레드2에서 먼저 연산 후에 
메인 메모리 값을 바꿔놓았기 때문이다.

이런 CAS 알고리즘 과정으로 멀티 스레드 상황에서
연산이 씹히는 문제를 해결할 수 있다.

요약하자면 

멀티스레드에서 가시성 문제 발생 ← 아토믹을 통해 해결가능 ← 아토믹은 CAS알고리즘을 통해서 가능.

Atomic(CAS)가 필요한 곳과 필요하지 않는 곳.

이제 우리는 atomic이 필요한 곳과 아닌 곳을 알 수 있다.

먼저 아토믹이 필요한 곳은, 가시성 문제가 발생하는 곳이다.

즉 여러 개 스레드가 동시 처리과정에서 레지스터 - 메인메모리 데이터 교환 간의 문제가 생겼을 때이다.

단일 스레드인 경우에는 Atomic, CAS알고리즘이 필요 없다.


Reference

https://soft.plusblog.co.kr/28

https://steady-coding.tistory.com/568

Atomic과 CAS 원리 이해

https://wannabe-gosu.tistory.com/29

https://velog.io/@syleemk/Java-Concurrent-Programming-%EA%B0%80%EC%8B%9C%EC%84%B1%EA%B3%BC-%EC%9B%90%EC%9E%90%EC%84%B1

https://steady-coding.tistory.com/568