스레드(Thread)와 스레드 안전(Thread-safe) 개념

dradra·2024년 10월 10일
post-thumbnail

스레드(thread)스레드 안전(thread-safe) 개념은 멀티스레드 환경에서 꼭 이해해야 할 중요한 개념들이다.

1. 스레드(Thread)의 기본 개념

  • 스레드프로세스 내에서 실행되는 작은 실행 단위이다. 우리가 흔히 사용하는 프로그램은 프로세스라고 부르고, 이 프로세스최소 하나 이상의 스레드를 가지고 있다.
  • 멀티스레딩(multithreading)은 하나의 프로세스 내에서 여러 스레드가 동시에 작업을 수행하는 방식이다. 각 스레드는 독립적으로 실행되지만, 프로세스 내에서 공유 자원(메모리, 파일 등)을 사용할 수 있다.

예시:

  • 웹 브라우저를 예로 들어보겠다. 브라우저가 동작할 때, 하나의 스레드는 사용자가 페이지를 스크롤할 때 반응하고, 또 다른 스레드는 서버로부터 데이터를 받아오는 작업을 처리한다. 여러 스레드가 동시에 동작하면 병렬 작업을 수행해서 작업 효율을 높일 수 있다.

public class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();

        thread1.start();  // 스레드 1 실행
        thread2.start();  // 스레드 2 실행
    }
}

이 코드에서는 두 개의 스레드가 동시에 실행돼서 병렬적으로 작업을 수행한다.

2. 스레드와 프로세스의 관계

  • 프로세스실행 중인 프로그램을 의미하며, 각 프로세스는 독립된 메모리 공간을 가지고 실행된다.
  • 스레드는 이러한 프로세스 내에서 실행되는 작은 단위로, 한 프로세스 내에는 여러 개의 스레드가 존재할 수 있다.
  • 모든 스레드는 프로세스의 자원을 공유하며, 여러 스레드가 협력하여 작업을 수행한다. 이러한 특징 덕분에 멀티스레드 환경에서는 자원을 공유하는 과정에서 경쟁 상태(race condition)와 같은 문제가 발생할 수 있다.

경쟁 상태(race condition)?


경쟁 상태는 여러 프로세스나 스레드가 동시에 공유 자원에 접근할 때, 예상치 못한 결과가 발생하는 상황을 말한다.

프로그래밍에서 특정 자원두 개 이상의 프로세스가 동시에 수정하려고 하면 동기화되지 않은 작업이 발생하면서 의도하지 않은 결과가 나올 수 있다.

3. 스레드 안전성(Thread-safe)의 기본 개념

  • 스레드 안전성이란, 여러 스레드가 동시에 하나의 자원(예: 변수, 객체 등)을 사용할 때 데이터가 손상되거나 충돌이 발생하지 않도록 보장하는 것이다.
  • 여러 스레드가 동시에 같은 데이터를 수정하려고 하면, 예기치 않은 결과가 발생할 수 있기 때문에 동기화 등의 기법을 사용해서 안전하게 처리해야 한다.
  • Thread-safe하지 않다는 건, 한 스레드가 공유 자원을 사용하는 도중에 다른 스레드가 그 자원에 접근해서 데이터가 엉키거나 손상될 수 있다는 뜻이다.

예시:

  • 두 명의 은행 직원이 동시에 같은 고객의 계좌에 접근해서 입출금 작업을 한다고 가정하겠다. 만약 동시에 입출금 작업이 이루어진다면, 한 직원이 작업한 내용을 다른 직원이 덮어쓰는 문제가 발생할 수 있다. 이게 Thread-safe하지 않은 상황이다.

Thread-safe 하지 않은 코드 예시

public class Counter {
    private int count = 0;

    public void increment() {
        count++;  // 여기에 여러 스레드가 동시에 접근하면 문제가 생긴다.
    }

    public int getCount() {
        return count;
    }
}

public class Main {
    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        thread1.start();
        thread2.start();
    }
}
  • 이 코드에서는 counter.increment() 메서드에 여러 스레드가 동시에 접근해서 값을 수정하려고 한다.
  • 그 결과로 데이터 손상이 발생할 수 있다.

4. 스레드 안전성을 보장하는 방법

동기화(Synchronization)

  • 동기화는 여러 스레드가 공유 자원에 접근할 때, 하나의 스레드만 그 자원에 접근할 수 있도록 제어하는 방식이다.
  • 이를 통해 경쟁 상태(race condition)를 방지할 수 있다.
public class Counter {
    private int count = 0;

    // synchronized 키워드를 사용해 스레드 안전성 보장
    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

여기서 synchronized 키워드increment 메서드에 동시에 하나의 스레드만 접근할 수 있도록 보장한다.
이렇게 하면 여러 스레드가 동시에 increment()를 호출해도 문제가 발생하지 않는다.

Lock 사용

synchronized는 단순하지만, 복잡한 상황에서는 더 세밀하게 제어할 수 있는 Lock 객체를 사용할 수 있다.

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

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

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();  // 마지막엔 항상 락을 해제해야 한다.
        }
    }

    public int getCount() {
        return count;
    }
}

여기서는 Lock명시적으로 사용해서, 하나의 스레드가 락을 잡고 작업을 완료할 때까지 다른 스레드는 그 자원에 접근하지 못하게 막는다.

5. 비동기 프로그래밍과 스레드 안전성

비동기 프로그래밍에서는 작업이 시작된 이후의 결과를 기다리지 않고 다른 작업을 수행할 수 있는 방식으로 동작한다. 비동기 작업은 이벤트 기반(event-driven)으로 이루어지며, 각 이벤트가 발생할 때 해당 작업이 처리된다.

이벤트 기반(Event-driven) 프로그래밍?

  • 이벤트 기반 프로그래밍이란, 프로그램의 흐름이 이벤트에 의해 결정되는 방식이다.
  • 예를 들어, 사용자가 버튼을 클릭하면 그 클릭 이벤트에 반응하여 특정 동작을 수행하는 구조이다.
  • 비동기 프로그래밍에서는 각 작업이 완료될 때 이벤트가 발생하고, 그 이벤트에 반응하는 코드가 실행된다.
  • 이를 통해 작업이 병렬적으로 수행되는 것처럼 느껴지며, 시스템의 응답성을 높일 수 있다.

하지만 멀티스레드 환경에서 비동기 작업과 스레드 동작이 결합되면, 여전히 스레드 안전성 문제를 고려해야 한다.

Atomic 클래스

  • 자바에서는 AtomicInteger, AtomicBoolean 같은 원자성(atomicity)을 보장하는 클래스를 제공한다.

    원자성이란?

    작업이 더 이상 나눌 수 없는 최소 단위로 수행되어 중간에 끼어들 수 없는 성질을 의미한다.
    즉, 원자적인 연산은 한 번에 실행되어 다른 스레드가 그 사이에 끼어들 수 없다.

  • Atomic 클래스들은 내부적으로 동기화 처리가 되어 있어, 멀티스레드 환경에서도 안전하게 사용할 수 있다.

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();  // 동기화된 방식으로 증가한다.
    }

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

이렇게 하면 동기화 키워드를 사용하지 않고도 스레드 안전성을 보장할 수 있다.

정리

스레드와 스레드 안전성은 멀티스레드 프로그래밍에서 매우 중요한 개념이다. 이를 안전하게 구현하기 위해 동기화(synchronized), Lock 객체, Atomic 클래스와 같은 다양한 방법을 사용할 수 있다. 비동기 프로그래밍은 응답성을 높이기 위한 유용한 기법이지만, 멀티스레드 환경에서 사용될 경우 여전히 스레드 안전성을 고려해야 한다.

0개의 댓글