2024년 10월 13일
조회 : 73이론
JAVA - 멀티 스레딩과 동기화
동기화(Synchronization)와 멀티쓰레딩(Multi-threading)는 자바에서 중요한 개념으로 병렬 처리를 통해 성능을 향상시키는 동시에 동시성 문제를 해결하기 위한 기술입니다. 멀티쓰레딩을 이해하고 적절한 동기화 기법을 사용하는 것이 성능 최적화와 안전한 자원 관리를 위해 필수적입니다. 아래에서 이 두 개념을 적절히 설명하고 사용 사례를 함께 다뤄보겠습니다.
스레드와 프로세스의 개념 복습
-
프로세스(Process): 운영체제에서 실행되는 하나의 독립된 프로그램입니다. 각 프로세스는 고유의 메모리 공간을 가지고 있으며 다른 프로세스와 메모리를 공유하지 않습니다. 하나의 프로세스는 여러 개의 스레드를 가질 수 있습니다.
-
스레드(Thread): 스레드는 프로세스 내에서 실행되는 작업 단위로 프로세스의 자원을 공유합니다. 즉 같은 프로세스 내의 스레드들은 같은 메모리 공간을 사용하며 스레드 간 자원 공유는 성능을 높일 수 있지만 동시성 문제를 발생시킬 수 있습니다.
멀티쓰레딩 사용 시 일반 코드와의 차이점 및 유의점
1. 일반 코드 vs 멀티쓰레딩 코드의 차이점
-
일반 코드: 기본적으로 순차적으로 실행됩니다. 하나의 작업이 끝난 후 다음 작업이 실행되며 병렬 처리 없이 단일 스레드만 사용합니다. CPU의 한 코어만 사용되며 병렬로 작업을 처리할 수 없습니다.
-
멀티쓰레딩 코드: 여러 스레드가 동시에 실행되므로 병렬 처리가 가능해집니다. 이를 통해 시스템의 여러 CPU 코어를 활용하여 성능을 극대화할 수 있습니다. 하지만 스레드 간 자원 공유로 인한 동시성 문제(Concurrency Issues)가 발생할 수 있으며 특히 여러 스레드가 동시에 같은 데이터를 읽고 쓸 때 **데이터 레이스(Data Race)**와 같은 문제가 발생할 수 있습니다.
2. 멀티스레딩 코드의 유의점
-
💥 동시성 문제(Concurrency Issues): 여러 스레드가 동시에 동일한 자원에 접근할 때 일관성이 보장되지 않으면 데이터가 손상될 수 있습니다. 예를 들어 두 스레드가 동일한 변수의 값을 동시에 변경하면 예상치 못한 결과를 초래할 수 있습니다.
-
⛔️ 데드락(Deadlock): 두 개 이상의 스레드가 서로 다른 자원을 점유한 상태에서 상대방이 가지고 있는 자원을 요청할 때 데드락이 발생할 수 있습니다. 데드락 상태에서는 스레드가 영원히 자원을 기다리며 멈추게 됩니다.
-
🎛️ 컨텍스트 스위칭(Context Switching): 여러 스레드가 실행될 때 CPU는 한 스레드에서 다른 스레드로 작업을 전환해야 합니다. 이 과정에서 컨텍스트 스위칭이 발생하며 이는 일정한 오버헤드를 발생시킵니다. 스레드 수가 많아지면 오버헤드도 커질 수 있습니다.
-
⛑️ 스레드 안전(Thread Safety): 여러 스레드가 동시에 접근하는 코드나 자원은 스레드 안전성을 보장해야 합니다. 이를 위해 **동기화(Synchronization)**를 사용하거나 스레드 안전한 자료구조(예:)를 사용해야 합니다.text
1ConcurrentHashMap
멀티스레딩의 장점과 단점
1. 장점
-
성능 향상: 멀티스레딩을 사용하면 여러 작업을 병렬로 처리할 수 있어 프로그램의 성능이 크게 향상됩니다. 특히 멀티코어 CPU에서 각각의 코어가 병렬로 작업을 수행하기 때문에 성능을 극대화할 수 있습니다.
-
응답성 개선: 사용자 인터페이스(UI) 프로그램에서 멀티스레딩을 사용하면 백그라운드 작업을 처리하는 동안에도 UI가 계속 반응할 수 있습니다. 예를 들어 파일을 다운로드하는 동안 UI가 멈추지 않고 사용자 입력에 반응할 수 있습니다.
-
자원 효율성: 멀티스레딩을 통해 여러 스레드가 동일한 메모리 공간과 자원을 공유하므로 멀티프로세스보다 자원을 효율적으로 사용할 수 있습니다.
2. 단점
-
동기화 문제: 여러 스레드가 동일한 자원에 동시에 접근할 때 동기화 문제로 인해 데이터 불일치나 데이터 손상이 발생할 수 있습니다. 이를 해결하기 위해 동기화 블록이나 **락(lock)**을 사용해야 합니다.
-
코드 복잡성 증가: 멀티스레딩 코드를 작성하면 프로그램의 복잡도가 크게 증가합니다. 특히 스레드 간의 타이밍 이슈나 동기화 문제를 해결하기 위한 코드가 많아지면서 디버깅과 테스트가 어려워질 수 있습니다.
-
컨텍스트 스위칭 오버헤드: 여러 스레드가 동시에 실행될 때 CPU는 스레드 간에 자주 전환해야 합니다. 이때 발생하는 컨텍스트 스위칭은 성능 저하를 초래할 수 있으며 스레드 수가 많아질수록 이 오버헤드도 증가합니다.
-
데드락 및 레이스 컨디션: 멀티스레딩 환경에서는 **데드락(Deadlock)**이나 레이스 컨디션(Race Condition) 같은 문제가 발생할 수 있습니다. 자원 접근을 적절히 관리하지 않으면 프로그램이 멈추거나 의도하지 않은 결과를 초래할 수 있습니다.
동기화(Synchronization)의 개념 및 중요성
1. 동기화의 개념
**동기화(Synchronization)**는 여러 스레드가 동시에 같은 자원에 접근할 때 그 자원의 일관성을 보장하기 위한 기술입니다. 이를 통해 한 스레드가 자원을 사용하는 동안 다른 스레드가 그 자원에 접근하지 못하게 하여 데이터 손상을 방지할 수 있습니다. => Blocking
자바에서 동기화는 키워드를 사용하여 구현할 수 있습니다.
text
1synchronized
-
메소드 동기화: 메소드 전체를 동기화하는 방법으로 메소드가 실행되는 동안 다른 스레드가 해당 메소드에 접근하지 못하게 합니다.java
1public synchronized void increment() { 2 count++; 3}
-
블록 동기화: 메소드의 일부만 동기화할 때 사용하며 필요한 코드 블록만 동기화하여 성능을 최적화할 수 있습니다.java
1public void increment() { 2 synchronized (this) { 3 count++; 4 } 5}
2. 동기화의 중요성
멀티스레딩 환경에서는 여러 스레드가 동시에 자원에 접근할 수 있기 때문에 동시성 문제가 발생합니다. 동기화를 적절히 사용하지 않으면 여러 스레드가 자원을 비정상적으로 처리하여 데이터를 손상시킬 수 있습니다. 동기화는 이러한 문제를 방지하고 프로그램의 안전성을 유지하는 데 필수적입니다.
🙄 개인 의견
멀티 스레딩 환경에서는 가능한 한 작고 필요한 부분에만 동기화
동기화는 멀티 스레딩의 병렬성을 살리면서 자원의 무결성을 지켜주지만 블로킹입니다.
따라서 서비스 메소드 자체에 사용하기 보단 함수를 원자화하거나 블록 동기화를 사용하는 것이 효율적으로 멀티 스레딩을 사용할 수 있는 것이라 생각합니다.
멀티스레딩과 동기화를 함께 고려한 코드 작성
예시: 동기화가 없는 경우의 문제
java1class Counter { 2 private int count = 0; 3 4 public void increment() { 5 count++; // 동기화되지 않음 6 } 7 8 public int getCount() { 9 return count; 10 } 11} 12 13public class Main { 14 public static void main(String[] args) { 15 Counter counter = new Counter(); 16 17 Thread t1 = new Thread(() -> { 18 for (int i = 0; i < 1000; i++) { 19 counter.increment(); 20 } 21 }); 22 23 Thread t2 = new Thread(() -> { 24 for (int i = 0; i < 1000; i++) { 25 counter.increment(); 26 } 27 }); 28 29 t1.start(); 30 t2.start(); 31 32 try { 33 t1.join(); 34 t2.join(); 35 } catch (InterruptedException e) { 36 e.printStackTrace(); 37 } 38 39 System.out.println("Final Count: " + counter.getCount()); 40 } 41}
위 코드에서는 동기화가 적용되지 않았기 때문에 두 스레드가 동시에 연산을 실행할 때 데이터가 손상될 수 있습니다.
text
1count++
🤨
그럼 어떻게 데이터가 손상 된다는 것일까요? 문제를 모르고 코드를 봤을 땐 count가 2000이 될거라 예상할 수 있습니다.
하지만 코드에서 두 개의 스레드가 동시에 count++ 연산을 실행하기 때문에 그 결과가 예상대로 2000이 되지 않는 이유는 스레드 간의 경쟁 상태(race condition) 때문입니다.
count++ 연산은 사실 세 가지 연산 으로 나뉩니다:
text11. count의 값을 읽음 (load) 22. count의 값을 1 증가시킴 (increment) 33. 증가된 값을 다시 count에 씀 (store)
이 세 가지 연산이 원자적으로 처리되지 않기 때문에, 두 스레드가 동일한 count 값을 읽고 동시에 1을 더한 값을 저장할 가능성이 있습니다. 예를 들어, 다음과 같은 상황이 발생할 수 있습니다:
text11. t1이 count 값을 읽어 0을 가져옴. 22. t2도 거의 동시에 count 값을 읽어 0을 가져옴. 33. t1이 count를 1로 증가시킴. 44. t2도 동일하게 1로 증가시킴. 55. 두 스레드 모두 1이라는 값을 count에 저장함.
이렇게 되면 실제로 두 번 증가해야 했던 count가 한 번만 증가하게 됩니다. 이러한 상황이 여러 번 반복되면, 최종적으로 예상되는 값인 2000이 아닌 더 작은 값이 출력됩니다.
이 문제는 여러 스레드가 동시에 count 변수를 접근할 때 발생하는 동기화 문제로, 이런 문제를 방지하려면 synchronized 키워드 또는 ReentrantLock 같은 동기화 도구를 사용하여 increment 메서드가 한 번에 하나의 스레드만 접근하도록 해야 합니다.
예시: 동기화 적용
동기화를 통해 위 문제를 해결할 수 있습니다.
java1class Counter { 2 private int count = 0; 3 4 public synchronized void increment() { 5 count++; // 동기화됨 6 } 7 8 public int getCount() { 9 return count; 10 } 11}
이제 메소드가 동기화되어 한 번에 하나의 스레드만 이 메소드를 실행할 수 있습니다. 이를 통해 데이터 손상을 방지할 수 있습니다.
text
1increment()
이렇게 동기화를 적용하면 각 스레드는 increment 메서드를 실행할 때 다른 스레드가 접근하지 못하게 하여 원자성(atomicity) 을 보장하게 됩니다.
결론
멀티스레딩은 성능 향상을 위한 중요한 기술이며 동시에 발생할 수 있는 동시성 문제를 해결하기 위해 동기화가 필요합니다. 동기화를 통해 자원을 안전하게 보호할 수 있지만 잘못된 사용은 성능 저하를 초래할 수 있습니다. 따라서 멀티스레딩과 동기화를 적절히 조화시키는 것이 중요합니다.
멀티스레딩과 동기화에 대한 깊은 이해는 안정적이고 효율적인 프로그램을 작성하는 데 필수적입니다.
수정삭제