본문 바로가기
Java Tutorials

#23 Concurrency 1

by xogns93 2024. 8. 5.

자바 공식 Concurrency 튜토리얼

 

Thread

스레드(Thread)란 프로세스 내에서 실행되는 독립적인 실행 흐름을 말합니다. 프로세스는 운영체제로부터 자원을 할당받아 실행되는 프로그램의 인스턴스를 의미하며, 각각의 프로세스는 하나 이상의 스레드를 가질 수 있습니다.

스레드는 프로세스 내에서 코드 실행의 기본 단위로, 동시에 여러 작업을 수행할 수 있습니다. 프로세스는 자신만의 주소 공간, 파일 핸들, 자원 등을 가지고 있지만, 스레드는 프로세스의 자원을 공유하여 실행됩니다. 따라서 스레드는 프로세스 내에서 동시에 여러 작업을 처리하고 서로 협력하여 작업을 완료할 수 있습니다.

스레드는 동시성 (Concurrency)을 제공하여 작업의 처리 속도를 향상시키고, 병렬성 (Parallelism)을 통해 여러 작업을 동시에 처리할 수 있습니다. 스레드를 사용하면 여러 작업을 동시에 수행하거나 하나의 작업을 여러 스레드로 분할하여 병렬로 처리할 수 있습니다.

스레드는 일반적으로 독립적인 실행 경로를 가지고 있으며, 각 스레드는 자체적인 프로그램 카운터(PC), 스택, 레지스터 등을 가지고 있습니다. 스레드는 동시에 실행되기 때문에 경쟁 상태와 같은 동시성 문제에 유의해야 하며, 적절한 동기화 메커니즘을 사용하여 데이터의 일관성과 안정성을 보장해야 합니다.

자바에서의 동시성(Concurrency)은 멀티태스킹을 소프트웨어 수준에서 구현하는 프로세스입니다. 이는 여러 작업을 동시에 실행하거나 처리하는 능력을 말하며, 특히 멀티코어 프로세서의 이점을 최대한 활용하여 애플리케이션의 성능을 향상시키는 데 중요합니다. 자바는 java.util.concurrent 패키지를 통해 강력한 동시성 프로그래밍 기능을 제공합니다.

자바 동시성의 주요 개념

  • 스레드(Thread): 자바에서 가장 기본적인 동시성 단위입니다. 스레드는 프로세스 내에서 실행되는 실행 흐름으로, 각 스레드는 프로세스의 자원을 공유하면서 독립적으로 명령을 실행할 수 있습니다.
  • 실행자(Executors): 자바 5부터 도입된 java.util.concurrent 패키지의 일부입니다. 스레드를 더 쉽게 관리하고 사용할 수 있도록 도와주는 여러 클래스와 인터페이스를 제공합니다. ExecutorService와 ScheduledExecutorService는 이 패키지에서 제공하는 중요한 인터페이스입니다.
  • 동기화(Synchronization): 여러 스레드가 공유 자원에 동시에 접근할 때 데이터의 일관성을 유지하기 위해 사용됩니다. synchronized 키워드와 Lock 인터페이스를 통해 구현할 수 있습니다.
  • Locks: java.util.concurrent.locks 패키지에는 Lock 인터페이스와 여러 구현체가 있습니다. 이들은 synchronized 키워드보다 더 세밀한 동기화 제어를 가능하게 합니다.
  • ThreadLocal: 스레드별로 변수를 격리하여 사용하게 해주는 유틸리티로, 각 스레드가 자신만의 변수 인스턴스를 가집니다.
  • Concurrent Collections: java.util.concurrent 패키지는 스레드 안전한 컬렉션을 제공합니다. 이 컬렉션들은 동시에 여러 스레드에서 접근해도 데이터 일관성을 유지할 수 있도록 설계되었습니다.
  • Future와 Callable: 비동기 연산의 결과를 표현하고 관리할 수 있는 방법을 제공합니다. Callable은 값을 반환할 수 있는 작업을 정의하고, Future는 그 작업의 결과를 나타냅니다.

 

동시성 프로그래밍의 이점과 과제

동시성 프로그래밍은 애플리케이션의 성능을 향상시키고, 리소스를 효율적으로 사용하며, 사용자에게 더 나은 반응성을 제공할 수 있게 해줍니다. 그러나 동시성은 복잡성과 버그 발생 가능성을 증가시키기 때문에, 동기화 문제, 경쟁 조건(race conditions), 교착 상태(deadlocks)와 같은 문제들을 관리해야 합니다. 따라서 동시성 프로그래밍은 주의 깊게 설계하고 구현해야 합니다.

 

Java Thread Synchronizaiton

자바는 스레드 동기화를 위해 다양한 메커니즘과 도구를 제공합니다. 주요한 동기화 메커니즘으로는 뮤텍스(Mutex)와 세마포어(Semaphore)가 있습니다. 자바에서 이러한 동기화 메커니즘을 지원하기 위해 다음과 같은 기능을 제공합니다:

1. synchronized 키워드: 자바에서 가장 기본적인 동기화 메커니즘으로, synchronized 키워드를 사용하여 특정 코드 블록 또는 메서드를 임계 영역(critical section)으로 지정할 수 있습니다. synchronized 키워드를 사용하면 해당 영역에는 오직 하나의 스레드만 진입할 수 있습니다.

2. 객체 잠금(Object Locking): 모든 자바 객체는 잠금(lock)을 가질 수 있습니다. synchronized 키워드를 사용하여 객체에 대한 잠금을 설정하고 해제할 수 있습니다. 이를 통해 여러 스레드가 동일한 객체를 안전하게 공유할 수 있습니다.

3. wait(), notify(), notifyAll() 메서드: Object 클래스에서 제공하는 메서드로, 스레드 간의 통신과 상태 제어를 위해 사용됩니다. wait() 메서드는 스레드를 일시적으로 대기 상태로 전환하고, notify() 또는 notifyAll() 메서드를 호출하여 대기 중인 스레드를 깨워 실행 상태로 전환할 수 있습니다.

4. Lock 및 Condition 인터페이스: 자바 5부터 도입된 java.util.concurrent 패키지에는 Lock 인터페이스와 Condition 인터페이스가 있습니다. Lock 인터페이스는 synchronized 키워드와 유사한 기능을 제공하며, Condition 인터페이스는 wait(), notify(), notifyAll() 메서드와 유사한 기능을 제공합니다. 이러한 인터페이스를 활용하여 더 세밀한 스레드 동기화를 구현할 수 있습니다.

뮤텍스와 세마포어는 자바의 표준 라이브러리에서 직접적으로 제공되지는 않지만, Lock 및 Condition 인터페이스를 사용하여 뮤텍스와 세마포어의 동작을 구현할 수 있습니다. 또한, java.util.concurrent 패키지에서는 CountDownLatch, CyclicBarrier, Phaser 등의 동기화 도구를 제공하여 다양한 동기화 시나리오를 구현할 수 있도록 도와줍니다.

Concurrency

컴퓨터 사용자는 자신의 시스템이 한 번에 두 가지 이상의 작업을 수행할 수 있다는 사실을 당연하게 여깁니다. 그들은 다른 응용 프로그램이 파일을 다운로드하고 인쇄 대기열을 관리하고 오디오를 스트리밍하는 동안 자신이 워드 프로세서에서 계속 작업할 수 있다고 가정합니다. 단일 응용 프로그램도 한 번에 둘 이상의 작업을 수행할 것으로 예상되는 경우가 많습니다. 예를 들어 스트리밍 오디오 애플리케이션은 동시에 네트워크에서 디지털 오디오를 읽고, 압축을 풀고, 재생을 관리하고, 디스플레이를 업데이트해야 합니다. 워드 프로세서도 아무리 바쁘게 텍스트를 재포맷하거나 디스플레이를 업데이트하더라도 키보드와 마우스 이벤트에 항상 응답할 준비가 되어 있어야 합니다. 이러한 작업을 수행할 수 있는 소프트웨어를 동시 소프트웨어라고 합니다. 

 

Java 플랫폼은 처음부터 Java 프로그래밍 언어 및 Java 클래스 라이브러리의 기본 동시성(concurrency) 지원과 함께 동시 프로그래밍을 지원하도록 설계되었습니다. 버전 5.0부터 Java 플랫폼에는 높은 수준의 동시성 API도 포함되었습니다. 이 학습에서는 플랫폼의 기본 동시성 지원을 소개하고 java.util.concurrent 패키지의 일부 고급 API를 요약합니다.

 

Processes and Threads

In concurrent programming, there are two basic units of execution: processes and threads. In the Java programming language, concurrent programming is mostly concerned with threads. However, processes are also important.

동시(concurrent) 프로그래밍에는 두 가지 기본 실행 단위인 processes threads가 있습니다. Java 프로그래밍 언어에서 동시 프로그래밍은 대부분 스레드와 관련이 있습니다. 그러나 프로세스도 중요합니다

 

컴퓨터 시스템에는 일반적으로 많은 활성 프로세스와 스레드가 있습니다. 이는 단일 실행 코어만 있는 시스템에서도 마찬가지이므로 주어진 순간에 실제로 실행되는 스레드는 하나만 있습니다. 단일 코어의 처리 시간은 타임 슬라이싱(time slicing)이라는 OS 기능을 통해 프로세스와 스레드 간에 공유됩니다.

 

컴퓨터 시스템에 다중 프로세서 또는 다중 실행 코어가 있는 프로세서가 있는 것이 점점 일반화되고 있습니다. 이것은 프로세스와 스레드의 동시 실행을 위한 시스템의 용량을 크게 향상시킵니다. 그러나 동시성은 다중 프로세서나 실행 코어가 없는 단순한 시스템에서도 가능합니다.

 

Processes

프로세스에는 독립적인 실행 환경이 있습니다. 일반적으로 프로세스에는 기본 런타임 리소스의 완전한 private 세트가 있습니다. 특히 각 프로세스에는 자신만의 메모리 공간이 있습니다.

프로세스는 종종 프로그램이나 응용 프로그램과 동의어로 간주됩니다. 그러나 사용자가 단일 응용 프로그램으로 보는 것은 실제로 협력 프로세스 집합일 수 있습니다. 프로세스 간 통신을 용이하게 하기 위해 대부분의 운영 체제는 pipes  sockets과 같은 IPC(Inter Process Communication) 리소스를 지원합니다. IPC는 동일한 시스템의 프로세스 간 통신뿐만 아니라 다른 시스템의 프로세스에도 사용됩니다.

 

대부분의 JVM(Java Virtual Machine) 구현은 단일 프로세스로 실행됩니다. Java 응용 프로그램은 ProcessBuilder 객체를 사용하여 추가 프로세스를 만들 수 있습니다. 다중 프로세스 응용 프로그램은 이 단원의 범위를 벗어납니다.

일반적으로 프로세스에는 기본 런타임 리소스의 완전한 private 세트가 있다는 말은, 각 프로세스가 운영 체제로부터 독립적인 실행 환경을 제공받는다는 것을 의미합니다. 이 독립적인 실행 환경에는 메모리 공간, 파일 핸들, 소켓 연결, 환경 변수 등 프로세스가 실행되는 데 필요한 모든 리소스가 포함됩니다. 각 프로세스는 이러한 리소스들을 다른 프로세스와 공유하지 않으며, 자신만의 독립된 공간에서 이를 관리하고 사용합니다.

기본 런타임 리소스의 예
1. 메모리 공간: 가장 중요한 리소스 중 하나로, 각 프로세스는 자신만의 가상 메모리 공간을 가집니다. 이 공간은 코드, 데이터, 스택, 힙 영역으로 구분되며, 프로세스 간에 격리되어 있어 한 프로세스의 메모리 영역이 다른 프로세스에 의해 직접적으로 접근되거나 수정될 수 없습니다.
2. 파일 핸들 및 디스크립터: 프로세스가 파일, 소켓, 파이프 등을 사용할 때, 운영 체제는 이를 관리하기 위해 핸들 또는 디스크립터를 할당합니다. 각 프로세스는 자신에게 할당된 핸들 또는 디스크립터를 통해 이러한 리소스에 접근합니다.
3. 소켓 연결: 네트워크 통신을 위해 사용되는 소켓도 프로세스별로 독립적으로 관리됩니다. 프로세스가 네트워크 소켓을 열면, 해당 소켓은 그 프로세스만의 리소스로서 운영됩니다.
4. 환경 변수: 프로세스는 시작될 때 운영 체제로부터 환경 변수의 복사본을 받습니다. 이는 프로세스 내에서만 접근 가능하며, 한 프로세스에서 환경 변수를 변경해도 다른 프로세스에는 영향을 주지 않습니다.

중요성
이러한 리소스의 프로세스별 독립성은 프로그램의 안정성과 보안을 보장하는 데 매우 중요합니다. 프로세스 간에 리소스가 완전히 분리되어 있기 때문에, 잘못된 코드나 악의적인 행위가 시스템 전체에 영향을 미치는 것을 방지할 수 있습니다. 예를 들어, 한 프로세스의 메모리 누수 문제가 다른 프로세스에 영향을 주지 않고, 프로세스 간에 데이터를 안전하게 격리할 수 있습니다.

 

Threads

스레드는 경량 프로세스라고도 합니다. 프로세스와 스레드 모두 실행 환경을 제공하지만 새 스레드를 생성하는 데 필요한 리소스는 새 프로세스를 생성하는 것보다 적습니다.

스레드는 프로세스 내에 존재합니다. 모든 프로세스에는 적어도 하나가 있습니다. 스레드는 메모리 및 open 파일을 포함하여 프로세스의 리소스를 공유합니다. 이것은 효율적이지만 잠재적으로 문제가 있는 communication을 만듭니다.

 

Multithreaded execution은 Java 플랫폼의 필수 기능입니다. 모든 애플리케이션에는 적어도 하나의 스레드가 있습니다. 메모리 관리 및 signal 처리와 같은 작업을 수행하는 "시스템" 스레드를 포함한다면 여러 개입니다. 그러나 애플리케이션 프로그래머의 관점에서 보면 메인 스레드라고 하는 단 하나의 스레드로 시작합니다. 이 스레드에는 다음 섹션에서 설명하는 것처럼 추가 스레드를 생성하는 기능이 있습니다.

 

Thread Objects

각 스레드는 Thread 클래스의 인스턴스와 연결됩니다. Thread 객체를 사용하여 동시(concurrent) 응용 프로그램을 만드는 두 가지 기본 전략이 있습니다.

  • 스레드 생성과 관리를 직접적으로 제어하려면, 애플리케이션이 비동기(asynchronous) 작업을 시작해야 할 때마다 Thread를 인스턴스화하면 됩니다.
  • 애플리케이션의 스레드 관리를 애플리케이션의 나머지로부터 추상화하기 위해, 애플리케이션의 작업을 executor에 전달하세요.

이 섹션에서는 Thread 객체의 사용에 대해 설명합니다. Executors는 다른 high-level concurrency objects와 논의됩니다.

 

Defining and Starting a Thread

Thread 인스턴스를 생성하는 애플리케이션은 해당 스레드에서 실행될 코드를 제공해야 합니다. 다음 두 가지 방법이 있습니다:

  • Runnable 객체를 제공합니다. Runnable 인터페이스는 스레드에서 실행되는 코드를 포함하기 위한 단일 메서드인 run을 정의합니다. Runnable 객체는 HelloRunnable 예제에서와 같이 Thread 생성자에 전달됩니다.
    public class HelloRunnable implements Runnable {
    
        public void run() {
            System.out.println("Hello from a thread!");
        }
    
        public static void main(String args[]) {
            (new Thread(new HelloRunnable())).start();
        }
    
    }
    
  • SubClass Thread. Thread 클래스 자체는 Runnable을 구현하지만 해당 run 메서드는 아무 작업도 수행하지 않습니다. 응용 프로그램은 HelloThread 예제에서와 같이 고유한 실행 구현을 제공하여 Thread를 하위 클래스로 만들 수 있습니다:
    public class HelloThread extends Thread {
    
        public void run() {
            System.out.println("Hello from a thread!");
        }
    
        public static void main(String args[]) {
            (new HelloThread()).start();
        }
    
    }
    

두 예제 모두 새 스레드를 시작하기 위해 Thread.start를 호출합니다.

어떤 관용구를 사용해야 할까요? 첫 번째 관용구는 Runnable 객체를 사용하므로 더 일반적입니다. Runnable 객체는 Thread 이외의 클래스를 상속할 수 있습니다. 두 번째 관용구는 간단한 애플리케이션에서 사용하기 쉽지만, 작업 클래스가 Thread의 하위 클래스여야 한다는 제한이 있습니다. 이 강의는 첫 번째 접근 방식에 초점을 맞추고 있으며, Runnable 작업을 실행하는 Thread 객체와 분리합니다. 이 접근 방식은 더 유연하며, 나중에 다룰 고수준 스레드 관리 API에도 적용할 수 있습니다.

 

The Thread class defines a number of methods useful for thread management. These include static methods, which provide information about, or affect the status of, the thread invoking the method. The other methods are invoked from other threads involved in managing the thread and Thread object. We'll examine some of these methods in the following sections.

Thread 클래스는 스레드 관리에 유용한 여러 메서드를 정의합니다. 여기에는 메서드를 호출하는 스레드에 대한 정보를 제공하거나 상태에 영향을 주는 정적 메서드가 포함됩니다. 다른 메서드는 스레드 및 스레드 객체 관리와 관련된 다른 스레드에서 호출됩니다. 다음 섹션에서 이러한 방법 중 일부를 살펴보겠습니다.

 

Pausing Execution with Sleep

Thread.sleep는 현재 스레드의 실행을 지정된 기간 동안 일시 중지시킵니다. 이는 어플리케이션 내의 다른 스레드나 컴퓨터 시스템에서 실행 중인 다른 어플리케이션에게 프로세서 시간을 제공하는 효율적인 방법입니다. sleep 메서드는 다음 예제에서 보여지는 것처럼 속도 조절에도 사용될 수 있으며, 나중에 다룰 SimpleThreads 예제와 같이 시간 요구 사항이 있는 작업을 수행하는 다른 스레드를 기다리기 위해서도 사용될 수 있습니다.

두 가지 오버로딩된 버전의 sleep 메서드가 제공됩니다. 하나는 밀리초 단위로 sleep 시간을 지정하고, 다른 하나는 나노초 단위로 sleep 시간을 지정합니다. 그러나 이러한 sleep 시간은 운영 체제에서 제공하는 기능에 의해 정확하게 보장되지 않을 수 있습니다. 또한, 나중에 다룰 섹션에서 볼 수 있듯이, 인터럽트에 의해 sleep 기간이 중단될 수도 있습니다. 어떤 경우에도 sleep을 호출하여 스레드가 정확히 지정된 시간 동안 일시 중지될 것이라고 가정해서는 안 됩니다.

 

SleepMessages 예제는 4초 간격으로 메시지를 출력하기 위해 sleep을 사용합니다.

package org.example.sleepwake;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        String importantInfo[] = {
                "Mares eat oats",
                "Does eat oats",
                "Little lambs eat ivy",
                "A kid will eat ivy too"
        };

        Thread messageThread = new Thread(() -> {
            try {
                System.out.println("Run message Thread");
                for (String info : importantInfo) {
                    // Pause for 4 seconds
                    Thread.sleep(4000);
                    // Print a message
                    System.out.println(info);
                }
            } catch (InterruptedException e) {
                System.out.println("Message thread was interrupted!");
            } finally {
                System.out.println("message thread good-bye");
            }
        });

        messageThread.start(); // 메시지 스레드 시작

        // 이 스레드는 메시지 스레드를 2초 후에 "깨우기" 위해 사용됩니다.
        Thread interruptingThread = new Thread(() -> {
            System.out.println("Run interrupt thread");
            try {
                Thread.sleep(10000); // 메시지 스레드 시작 후 10초 기다림
                messageThread.interrupt(); // 메시지 스레드를 깨움
            } catch (InterruptedException e) {
                System.out.println("Interrupting thread was interrupted!");
            }
        });

        interruptingThread.start(); // 깨우는 스레드 시작
    }
}

주목해야 할 점은 main 메서드가 InterruptedException을 선언한다는 것입니다. 이는 sleep이 활성화된 동안 다른 스레드가 현재 스레드를 인터럽트할 때 sleep이 throw하는 예외입니다. 이 어플리케이션에서는 인터럽트를 발생시킬 다른 스레드를 정의하지 않았으므로 InterruptedException을 catch하지 않습니다.

 

Interrupts

interrupt 는 스레드에게 현재 진행 중인 작업을 중지하고 다른 작업을 수행해야 함을 알리는 신호입니다. 스레드가 인터럽트에 대해 어떻게 반응할지는 프로그래머가 결정해야 합니다. 그러나 스레드가 종료되는 것이 매우 일반적입니다. 이것은 이 강의에서 강조된 사용 방법입니다.

스레드는 인터럽트될 스레드에 대한 Thread 객체의 interrupt 메서드를 호출하여 인터럽트를 보냅니다. 인터럽트 메커니즘이 올바르게 작동하려면 인터럽트된 스레드가 자체 인터럽트를 지원해야 합니다.

 

 

Supporting Interruption

스레드가 자체 인터럽션을 지원하는 방법은 스레드가 현재 어떤 작업을 수행하고 있는지에 따라 다릅니다. 스레드가 InterruptedException을 throw하는 메서드를 자주 호출하는 경우, 해당 예외를 catch한 후 run 메서드에서 그냥 리턴하면 됩니다. 예를 들어, SleepMessages 예제의 중앙 메시지 루프가 스레드의 Runnable 객체의 run 메서드 내에 있다고 가정해봅시다. 그렇다면 인터럽트를 지원하도록 다음과 같이 수정될 수 있습니다.

for (int i = 0; i < importantInfo.length; i++) {
    // Pause for 4 seconds
    try {
        Thread.sleep(4000);
    } catch (InterruptedException e) {
        // We've been interrupted: no more messages.
        return;
    }
    // Print a message
    System.out.println(importantInfo[i]);
}

sleep와 같이 InterruptedException을 throw하는 많은 메서드들은, 인터럽트를 받았을 때 현재 작업을 취소하고 즉시 리턴하기 위해 설계되었습니다.

 

만약 스레드가 InterruptedException을 throw하는 메서드를 호출하지 않고 오랜 시간을 보내는 경우, 주기적으로 Thread.interrupted를 호출해야 합니다. Thread.interrupted는 인터럽트가 수신되었다면 true를 반환합니다. 

예를 들어:

for (int i = 0; i < inputs.length; i++) {
    heavyCrunch(inputs[i]);
    if (Thread.interrupted()) {
        // We've been interrupted: no more crunching.
        return;
    }
}

이 간단한 예제에서는 코드가 단순히 인터럽트를 테스트하고, 인터럽트가 수신되었다면 스레드를 종료합니다. 더 복잡한 애플리케이션에서는 InterruptedException을 throw하는 것이 더 의미가 있을 수 있습니다:

if (Thread.interrupted()) {
    throw new InterruptedException();
}

이는 인터럽트 처리 코드를 catch 절에서 중앙 집중화할 수 있도록 해줍니다.

 

The Interrupt Status Flag

인터럽트 메커니즘은 interrupt status라고 알려진 내부 플래그(flag)를 사용하여 구현됩니다. Thread.interrupt를 호출하면 이 플래그가 set됩니다. 스레드가 정적 메서드 Thread.interrupted를 호출하여 인터럽트를 확인할 때는 인터럽트 상태가 clear 됩니다. 다른 스레드의 인터럽트 상태를 조회하기 위해 사용되는 non-static isInterrupted 메서드는 인터럽트 상태 플래그를 변경하지 않습니다. 

 

관례적으로, InterruptedException을 throw하고 메서드를 종료하는 경우, 해당 메서드는 인터럽트 상태를 clear 하게 됩니다. 그러나 다른 스레드가 interrupt를 호출하여 인터럽트 상태가 다시 set될 수 있다는 점을 항상 염두에 두어야 합니다.

 

Joins

The join method allows one thread to wait for the completion of another.

join 메서드는 한 스레드가 다른 스레드의 완료를 기다리도록 허용합니다.

If t is a Thread object whose thread is currently executing,

t가 현재 실행 중인 스레드를 가진 Thread 객체인 경우,

t.join();

causes the current thread to pause execution until t's thread terminates. Overloads of join allow the programmer to specify a waiting period. However, as with sleep, join is dependent on the OS for timing, so you should not assume that join will wait exactly as long as you specify.

현재 스레드를 일시 중지시켜 t의 스레드가 종료될 때까지 기다리게 합니다. join의 오버로드를 사용하면 프로그래머가 대기 기간을 지정할 수 있습니다. 그러나 sleep과 마찬가지로 join은 타이밍에 대해 운영 체제에 의존하므로 join이 정확히 지정한 시간만큼 기다릴 것이라고 가정해서는 안 됩니다.

 

Like sleep, join responds to an interrupt by exiting with an InterruptedException.

sleep와 마찬가지로, join도 InterruptedException을 발생시켜 인터럽트에 응답합니다.

 

The SimpleThreads Example

The following example brings together some of the concepts of this section. SimpleThreads consists of two threads. The first is the main thread that every Java application has. The main thread creates a new thread from the Runnable object, MessageLoop, and waits for it to finish. If the MessageLoop thread takes too long to finish, the main thread interrupts it.

다음 예제는 이 섹션의 개념들을 종합적으로 보여줍니다. SimpleThreads는 두 개의 스레드로 구성됩니다. 첫 번째는 모든 Java 어플리케이션에 있는 main 스레드입니다. main 스레드는 Runnable 객체인 MessageLoop에서 새로운 스레드를 생성하고, 이 스레드의 종료를 기다립니다. 만약 MessageLoop 스레드가 종료하는 데에 너무 오랜 시간이 걸린다면, main 스레드는 해당 스레드를 인터럽트합니다.

 

The MessageLoop thread prints out a series of messages. If interrupted before it has printed all its messages, the MessageLoop thread prints a message and exits.

MessageLoop 스레드는 일련의 메시지를 출력합니다. 만약 모든 메시지를 출력하기 전에 인터럽트가 발생하면, MessageLoop 스레드는 메시지를 출력하고 종료합니다.

public class SimpleThreads {

    // Display a message, preceded by
    // the name of the current thread
    static void threadMessage(String message) {
        String threadName =
            Thread.currentThread().getName();
        System.out.format("%s: %s%n",
                          threadName,
                          message);
    }

    private static class MessageLoop
        implements Runnable {
        public void run() {
            String importantInfo[] = {
                "Mares eat oats",
                "Does eat oats",
                "Little lambs eat ivy",
                "A kid will eat ivy too"
            };
            try {
                for (int i = 0;
                     i < importantInfo.length;
                     i++) {
                    // Pause for 4 seconds
                    Thread.sleep(4000);
                    // Print a message
                    threadMessage(importantInfo[i]);
                }
            } catch (InterruptedException e) {
                threadMessage("I wasn't done!");
            }
        }
    }

    public static void main(String args[])
        throws InterruptedException {

        // Delay, in milliseconds before
        // we interrupt MessageLoop
        // thread (default one hour).
        long patience = 1000 * 60 * 60;

        // If command line argument
        // present, gives patience
        // in seconds.
        if (args.length > 0) {
            try {
                patience = Long.parseLong(args[0]) * 1000;
            } catch (NumberFormatException e) {
                System.err.println("Argument must be an integer.");
                System.exit(1);
            }
        }

        threadMessage("Starting MessageLoop thread");
        long startTime = System.currentTimeMillis();
        Thread t = new Thread(new MessageLoop());
        t.start();

        threadMessage("Waiting for MessageLoop thread to finish");
        // loop until MessageLoop
        // thread exits
        while (t.isAlive()) {
            threadMessage("Still waiting...");
            // Wait maximum of 1 second
            // for MessageLoop thread
            // to finish.
            t.join(1000);
            if (((System.currentTimeMillis() - startTime) > patience)
                  && t.isAlive()) {
                threadMessage("Tired of waiting!");
                t.interrupt();
                // Shouldn't be long now
                // -- wait indefinitely
                t.join();
            }
        }
        threadMessage("Finally!");
    }
}

 

Synchronization

Threads communicate primarily by sharing access to fields and the objects reference fields refer to. This form of communication is extremely efficient, but makes two kinds of errors possible: thread interference and memory consistency errors. The tool needed to prevent these errors is synchronization.

Threads는 주로 필드 및 객체 참조 필드에 대한 액세스를 공유함으로써 통신합니다. 이러한 형태의 통신은 매우 효율적이지만, 스레드간 간섭(thread interference)과 메모리 일관성 오류(memory consistency errors)라는 두 가지 종류의 오류가 발생할 수 있습니다. 이러한 오류를 방지하기 위해 필요한 도구는 동기화(synchronization)입니다.

 

However, synchronization can introduce thread contention, which occurs when two or more threads try to access the same resource simultaneously and cause the Java runtime to execute one or more threads more slowly, or even suspend their execution. Starvation and livelock are forms of thread contention. See the section Liveness for more information.

그러나 동기화는 스레드 경합(thread contention)을 발생시킬 수 있습니다. 스레드 경합은 두 개 이상의 스레드가 동시에 동일한 리소스에 액세스하려고 할 때 발생하며, 이는 자바 런타임이 하나 이상의 스레드를 더 느리게 실행하거나 실행을 일시 중단할 수 있습니다. 스레드 경합의 형태로는 굶주림(starvation)과 라이브락(livelock)이 있습니다. 자세한 내용은 Liveness 섹션을 참조하십시오.

 

This section covers the following topics:

  • Thread Interference 는 여러 스레드가 공유 데이터에 접근할 때 오류가 발생하는 방식을 설명합니다.
  • Memory Consistency Errors 는 공유 메모리의 일관성이 없는 뷰로 인해 발생하는 오류를 설명합니다.
  • Synchronized Methods 는 스레드 간 간섭(thread interference)과 메모리 일관성 오류(memory consistency errors)를 효과적으로 방지할 수 있는 간단한 관용구를 설명합니다.
  • Implicit Locks and Synchronization 는 보다 일반적인 동기화 관용구를 설명하며, 동기화가 암묵적인 잠금(implicit locks)에 기반한다고 설명합니다.
  • Atomic Access 는 다른 스레드에 의해 간섭받을 수 없는 작업의 일반적인 개념에 대해 설명합니다..

 

Thread Interference

Consider a simple class called Counter

class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}

Counter is designed so that each invocation of increment will add 1 to c, and each invocation of decrement will subtract 1 from c. However, if a Counter object is referenced from multiple threads, interference between threads may prevent this from happening as expected.

Counter는 increment를 호출할 때마다 c에 1을 더하고, decrement를 호출할 때마다 c에서 1을 뺄 수 있도록 설계되었습니다. 그러나 Counter 객체가 여러 스레드에서 참조된다면, 스레드 간의 간섭으로 인해 이러한 동작이 예상대로 이루어지지 않을 수 있습니다.

 

Interference happens when two operations, running in different threads, but acting on the same data, interleave. This means that the two operations consist of multiple steps, and the sequences of steps overlap.

간섭은 서로 다른 스레드에서 실행되지만 동일한 데이터에 작용하는 두 개의 작업이 교차(interleave)하는 경우 발생합니다. 이는 두 작업이 여러 단계로 구성되어 있고, 단계의 순서가 겹치는 것을 의미합니다.

 

It might not seem possible for operations on instances of Counter to interleave, since both operations on c are single, simple statements. However, even simple statements can translate to multiple steps by the virtual machine. We won't examine the specific steps the virtual machine takes — it is enough to know that the single expression c++ can be decomposed into three steps:

Counter의 인스턴스에 대한 작업이 교차(interleave)할 수 있는 것처럼 보이지 않을 수 있습니다. 왜냐하면 c에 대한 두 작업 모두 단일하고 간단한 문장이기 때문입니다. 그러나 심지어 간단한 문장도 가상 머신에 의해 여러 단계로 변환될 수 있습니다. 가상 머신이 수행하는 구체적인 단계를 살펴볼 필요는 없습니다. 중요한 점은 단일 표현식인 c++도 세 단계로 분해될 수 있다는 것입니다.

  1. Retrieve the current value of c.
  2. Increment the retrieved value by 1.
  3. Store the incremented value back in c.

The expression c-- can be decomposed the same way, except that the second step decrements instead of increments.

표현식 c--도 동일한 방식으로 분해될 수 있지만, 두 번째 단계에서 감소(increment) 대신 감소(decrement)가 일어납니다.

 

Thread A가 increment를 호출하는 동안 Thread B가 decrement를 호출한다고 가정해봅시다. c의 초기 값이 0이라면, 이들 교차 작업은 다음과 같은 시퀀스를 따를 수 있습니다:

  1. Thread A: Retrieve c.
  2. Thread B: Retrieve c.
  3. Thread A: Increment retrieved value; result is 1.
  4. Thread B: Decrement retrieved value; result is -1.
  5. Thread A: Store result in c; c is now 1.
  6. Thread B: Store result in c; c is now -1.

Thread A's result is lost, overwritten by Thread B. This particular interleaving is only one possibility. Under different circumstances it might be Thread B's result that gets lost, or there could be no error at all. Because they are unpredictable, thread interference bugs can be difficult to detect and fix.

Thread A의 결과는 Thread B에 의해 덮어씌워지므로 손실됩니다. 이러한 특정한 교차 작업은 하나의 가능성에 불과합니다. 다른 상황에서는 Thread B의 결과가 손실될 수 있거나, 오류가 전혀 발생하지 않을 수도 있습니다. 예측할 수 없기 때문에, 스레드 간 간섭 버그는 감지하고 수정하기 어려울 수 있습니다.

 

Memory Consistency Errors

Memory consistency errors occur when different threads have inconsistent views of what should be the same data. The causes of memory consistency errors are complex and beyond the scope of this tutorial. Fortunately, the programmer does not need a detailed understanding of these causes. All that is needed is a strategy for avoiding them.

메모리 일관성 오류는 서로 다른 스레드가 동일한 데이터로 간주되어야 할 것에 대해 일관되지 않은 보기를 가질 때 발생합니다. 메모리 일관성 오류의 원인은 복잡하며 이 튜토리얼의 범위를 벗어납니다. 다행히도, 프로그래머는 이러한 원인에 대해 상세한 이해가 필요하지 않습니다. 필요한 것은 이러한 오류를 피하기 위한 전략입니다.

 

The key to avoiding memory consistency errors is understanding the happens-before relationship. This relationship is simply a guarantee that memory writes by one specific statement are visible to another specific statement. To see this, consider the following example. Suppose a simple int field is defined and initialized:

메모리 일관성 오류를 방지하는 핵심은 happens-before(발생 전) 관계를 이해하는 것입니다. 이 관계는 단순히 하나의 특정 statement에 의한 메모리 쓰기가 다른 특정 statement에서 볼 수 있음을 보장합니다. 이를 확인하려면 다음 예를 고려하십시오. 간단한 int 필드가 정의되고 초기화되었다고 가정합니다.

int counter = 0;

The counter field is shared between two threads, A and B. Suppose thread A increments counter:

counter 필드는 두 개의 스레드 A와 B 사이에서 공유됩니다. 스레드 A가 counter를 증가시킨다고 가정해봅시다:

counter++;

Then, shortly afterwards, thread B prints out counter:

그런 다음, 잠시 후에 스레드 B가 counter를 print합니다:

System.out.println(counter);

If the two statements had been executed in the same thread, it would be safe to assume that the value printed out would be "1". But if the two statements are executed in separate threads, the value printed out might well be "0", because there's no guarantee that thread A's change to counter will be visible to thread B — unless the programmer has established a happens-before relationship between these two statements.

위 두 개의 statements이 동일한 스레드에서 실행된 경우, 출력된 값이 "1"이라고 가정하는 것이 안전합니다. 그러나 두 statements이 별도의 스레드에서 실행되는 경우 스레드 A의 카운터 변경 사항이 스레드 B에 표시된다는 보장이 없기 때문에 출력되는 값은 "0"일 수 있습니다. 

There are several actions that create happens-before relationships. One of them is synchronization, as we will see in the following sections.

이때, 프로그래머가 이 두 문장 사이에 happens-before 관계를 설정하지 않은 한, 값의 가시성을 보장할 수 없습니다.

We've already seen two actions that create happens-before relationships.

이미 두 가지 happens-before 관계를 생성하는 동작을 보았습니다.

  • 한 statement가 Thread.start를 호출할 때, 그 statement과 happens-before 관계가 있는 모든 statement은 새로운 스레드에 의해 실행되는 모든 문장과도 happens-before 관계가 있습니다. 새로운 스레드를 생성하는 코드의 효과는 새로운 스레드에게도 보이게 됩니다.
  • 한 스레드가 종료되고 다른 스레드에서 Thread.join이 반환되면, 종료된 스레드에 의해 실행된 모든 statement은 성공적인 join 이후에 실행되는 모든 statement과 happens-before 관계가 있습니다. 종료된 스레드의 코드 효과는 이제 join을 수행한 스레드에게도 보이게 됩니다.

For a list of actions that create happens-before relationships, refer to the Summary page of the java.util.concurrent package..

  

Synchronized Methods

The Java programming language provides two basic synchronization idioms: synchronized methods and synchronized statements. The more complex of the two, synchronized statements, are described in the next section. This section is about synchronized methods.

Java 프로그래밍 언어는 두 가지 기본 synchronization 관용구를 제공합니다: synchronized methods synchronized statements입니다. 이 중 더 복잡한 synchronized statements은 다음 섹션에서 설명됩니다. 이 섹션은 synchronized methods에 대한 내용입니다.

To make a method synchronized, simply add the synchronized keyword to its declaration:

메서드를 동기화하기 위해서는 해당 메서드의 선언에 단순히 synchronized 키워드를 추가하면 됩니다:

public class SynchronizedCounter {
    private int c = 0;

    public synchronized void increment() {
        c++;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }
}

If count is an instance of SynchronizedCounter, then making these methods synchronized has two effects:

count가 SynchronizedCounter의 인스턴스라면, 이러한 메서드를 동기화하면 두 가지 효과가 있습니다:

  • First, it is not possible for two invocations of synchronized methods on the same object to interleave. When one thread is executing a synchronized method for an object, all other threads that invoke synchronized methods for the same object block (suspend execution) until the first thread is done with the object. 첫째로, 동일한 객체에 대해 synchronized methods의 두 호출이 교차(interleave)하는 것은 불가능합니다. 하나의 스레드가 객체의 synchronized method를 실행하는 동안, 해당 객체에 대해 synchronized methods을 호출하는 다른 모든 스레드는 첫 번째 스레드가 해당 객체 작업을 완료할 때까지 블록(Block)되어 (실행이 중단되어) 대기하게 됩니다.
  • Second, when a synchronized method exits, it automatically establishes a happens-before relationship with any subsequent invocation of a synchronized method for the same object. This guarantees that changes to the state of the object are visible to all threads. 둘째로, synchronized method가 종료되면 자동으로 동일한 객체에 대한 후속 synchronized method의 호출과 happens-before 관계를 설정합니다. 이를 통해 객체의 상태 변경이 모든 스레드에게 보이도록 보장됩니다.

Note that constructors cannot be synchronized — using the synchronized keyword with a constructor is a syntax error. Synchronizing constructors doesn't make sense, because only the thread that creates an object should have access to it while it is being constructed.

생성자는 동기화될 수 없습니다. 생성자에 synchronized 키워드를 사용하는 것은 구문 오류입니다. 생성자를 동기화하는 것은 의미가 없습니다. 왜냐하면 객체가 생성되는 동안 해당 객체에 대한 액세스 권한은 해당 객체를 생성하는 스레드에게만 있어야하기 때문입니다.


Warning: When constructing an object that will be shared between threads, be very careful that a reference to the object does not "leak" prematurely. For example, suppose you want to maintain a List called instances containing every instance of class. You might be tempted to add the following line to your constructor:

스레드 간에 공유할 객체를 구성할 때 개체에 대한 참조가 조기에 "유출"되지 않도록 매우 주의하십시오. 예를 들어 클래스의 모든 인스턴스를 포함하는 instances라고 하는 List을 유지하려고 한다고 가정합니다. 생성자에 다음 줄을 추가하고 싶을 수도 있습니다.

instances.add(this);
But then other threads can use instances to access the object before construction of the object is complete.
하지만 그렇게 되면 객체의 생성이 완료되기 전에 다른 스레드가 instances를 통해 객체에 액세스할 수 있게 됩니다.

Synchronized methods enable a simple strategy for preventing thread interference and memory consistency errors: if an object is visible to more than one thread, all reads or writes to that object's variables are done through synchronized methods. (An important exception: final fields, which cannot be modified after the object is constructed, can be safely read through non-synchronized methods, once the object is constructed) This strategy is effective, but can present problems with liveness, as we'll see later in this lesson.

Synchronized methods는 스레드간 간섭과 메모리 일관성 오류를 방지하기 위한 간단한 전략을 제공합니다. 객체가 여러 스레드에게 보이는(표시되는) 경우, 해당 객체의 변수에 대한 모든 읽기 또는 쓰기 작업은 동기화된 메서드를 통해 수행됩니다. (중요한 예외: 객체가 생성된 후 수정할 수 없는 final 필드는 객체가 생성된 후에는 동기화되지 않은 메서드를 통해 안전하게 읽을 수 있습니다) 이 전략은 효과적이지만, 활성성(liveness)과 관련하여 나중에 이 강의에서 더 자세히 살펴볼 수 있는 문제가 발생할 수 있습니다.

 

Intrinsic Locks and Synchronization

Synchronization is built around an internal entity known as the intrinsic lock or monitor lock. (The API specification often refers to this entity simply as a "monitor.") Intrinsic locks play a role in both aspects of synchronization: enforcing exclusive access to an object's state and establishing happens-before relationships that are essential to visibility.

동기화는 고유 락(intrinsic lock) 또는 모니터 잠금(monitor lock)으로 알려진 내부 엔터티를 중심으로 구축됩니다. (API 사양에서는 종종 이 엔터티를 단순히 "모니터"라고 합니다.) 고유 락은 동기화의 두 가지 측면에서 모두 역할을 합니다. 즉, 객체 상태에 대한 배타적 액세스를 적용하고 가시성에 필수적인 사전 발생(happens-before) 관계를 설정하는 것입니다.

동기화의 양쪽 측면이란, 동기화의 두 가지 주요 목적을 의미합니다.

첫째로, 고유 락은 객체의 상태에 대한 독점적인 액세스를 보장합니다. 동기화된 메서드나 동기화된 블록에 진입한 스레드는 해당 객체의 고유 락을 획득하고, 다른 스레드들은 동시에 동기화된 메서드나 동기화된 블록을 실행할 수 없습니다. 이를 통해 여러 스레드가 동시에 객체의 상태를 수정하거나 일관성 없는 동작을 수행하는 것을 방지합니다.

둘째로, 고유 락은 가시성에 필요한 happens-before 관계를 설정합니다. 스레드가 동기화된 메서드나 동기화된 블록을 통해 고유 락을 획득하면, 그 스레드는 이전에 획득한 고유 락을 보유한 다른 스레드의 작업 결과를 볼 수 있습니다. 이를 통해 스레드 사이의 작업 순서와 상태의 일관성을 보장합니다.

즉, 고유 락은 동기화의 양쪽 측면, 즉 상호 배제와 가시성을 위한 핵심 역할을 수행합니다.

 

Every object has an intrinsic lock associated with it. By convention, a thread that needs exclusive and consistent access to an object's fields has to acquire the object's intrinsic lock before accessing them, and then release the intrinsic lock when it's done with them. A thread is said to own the intrinsic lock between the time it has acquired the lock and released the lock. As long as a thread owns an intrinsic lock, no other thread can acquire the same lock. The other thread will block when it attempts to acquire the lock.

모든 객체에는 관련 고유 락이 있습니다. 규칙에 따라 객체의 필드에 대한 배타적이고 일관된 액세스가 필요한 스레드는 액세스하기 전에 객체의 고유 락을 획득한 다음 작업이 완료되면 고유 락을 해제해야 합니다. 스레드는 락을 획득한 시간과 락을 해제한 시간 사이에 고유 락을 소유한다고 합니다. 스레드가 고유 락을 소유하는 한 다른 스레드는 동일한 락을 획득할 수 없습니다. 다른 스레드는 락을 획득하려고 시도할 때 차단됩니다.

When a thread releases an intrinsic lock, a happens-before relationship is established between that action and any subsequent acquisition of the same lock.

스레드가 고유 락을 해제하면 해당 작업과 동일한 락의 후속 획득 간에 사전 발생 관계가 설정됩니다.

Locks In Synchronized Methods

When a thread invokes a synchronized method, it automatically acquires the intrinsic lock for that method's object and releases it when the method returns. The lock release occurs even if the return was caused by an uncaught exception.

스레드가 동기화된 메서드를 호출하면 해당 메서드의 객체에 대한 고유 락을 자동으로 획득하고 메서드가 반환될 때 락을 해제합니다. 반환이 포착되지 않은 예외로 인해 발생한 경우에도 락 해제가 발생합니다.

You might wonder what happens when a static synchronized method is invoked, since a static method is associated with a class, not an object. In this case, the thread acquires the intrinsic lock for the Class object associated with the class. Thus access to class's static fields is controlled by a lock that's distinct from the lock for any instance of the class.

정적 메서드는 객체가 아닌 클래스와 연결되기 때문에 동기화된 정적 메서드가 호출되면 어떤 일이 발생하는지 궁금할 수 있습니다. 이 경우 스레드는 클래스와 연결된 클래스 객체에 대한 고유 락을 획득합니다. 따라서 클래스의 정적 필드에 대한 액세스는 클래스의 모든 인스턴스에 대한 락과는 다른 에 의해 제어됩니다.

Synchronized Statements

Another way to create synchronized code is with synchronized statements. Unlike synchronized methods, synchronized statements must specify the object that provides the intrinsic lock:

동기화된 코드를 만드는 또 다른 방법은 synchronized statements을 사용하는 것입니다. synchronized methods와 달리 동기화된 문은 고유 락을 제공하는 체를 지정해야 합니다.

public void addName(String name) {
    synchronized(this) {
        lastName = name;
        nameCount++;
    }
    nameList.add(name);
}

In this example, the addName method needs to synchronize changes to lastName and nameCount, but also needs to avoid synchronizing invocations of other objects' methods. (Invoking other objects' methods from synchronized code can create problems that are described in the section on Liveness.) Without synchronized statements, there would have to be a separate, unsynchronized method for the sole purpose of invoking nameList.add.

이 예에서 addName 메소드는 lastName 및 nameCount에 대한 변경 사항을 동기화해야 하지만 다른 객체의 메소드 호출 동기화를 피해야 합니다. (동기화된 코드에서 다른 개체의 메서드를 호출하면 Liveness 섹션에 설명된 문제가 발생할 수 있습니다.) synchronized statements이 없으면 nameList.add를 호출하기 위한 목적으로만 동기화되지 않은 별도의 메서드가 있어야 합니다.

Synchronized statements are also useful for improving concurrency with fine-grained synchronization. Suppose, for example, class MsLunch has two instance fields, c1 and c2, that are never used together. All updates of these fields must be synchronized, but there's no reason to prevent an update of c1 from being interleaved with an update of c2 — and doing so reduces concurrency by creating unnecessary blocking. Instead of using synchronized methods or otherwise using the lock associated with this, we create two objects solely to provide locks.

Synchronized statements은 세분화된 동기화로 동시성을 개선하는 데에도 유용합니다. 예를 들어 MsLunch 클래스에 함께 사용되지 않는 두 개의 인스턴스 필드 c1과 c2가 있다고 가정합니다. 이러한 필드의 모든 업데이트는 동기화되어야 하지만 c1 업데이트가 c2 업데이트와 인터리브되는 것을 방지할 이유가 없으며 이렇게 하면 불필요한 차단이 생성되어 동시성이 감소합니다. 동기화된 메서드를 사용하거나 이와 관련된 락을 사용하는 대신 락을 제공하기 위해 두 개의 객체를 만듭니다.

public class MsLunch {
    private long c1 = 0;
    private long c2 = 0;
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    public void inc1() {
        synchronized(lock1) {
            c1++;
        }
    }

    public void inc2() {
        synchronized(lock2) {
            c2++;
        }
    }
}

Use this idiom with extreme care. You must be absolutely sure that it really is safe to interleave access of the affected fields.

이 관용구는 매우 주의해서 사용하세요. 영향을 받는 필드의 액세스를 인터리브하는 것이 실제로 안전한지 절대적으로 확신해야 합니다.

Reentrant Synchronization

Recall that a thread cannot acquire a lock owned by another thread. But a thread can acquire a lock that it already owns. Allowing a thread to acquire the same lock more than once enables reentrant synchronization. This describes a situation where synchronized code, directly or indirectly, invokes a method that also contains synchronized code, and both sets of code use the same lock. Without reentrant synchronization, synchronized code would have to take many additional precautions to avoid having a thread cause itself to block.

스레드는 다른 스레드가 소유한 락을 획득할 수 없습니다. 그러나 스레드는 이미 소유하고 있는 락을 획득할 수 있습니다. 스레드가 동일한 락을 두 번 이상 획득하도록 허용하면 reentrant synchronization가 활성화됩니다. 이는 동기화된 코드가 직접 또는 간접적으로 동기화된 코드도 포함하는 메서드를 호출하고 두 코드 세트 모두 동일한 락을 사용하는 상황을 설명합니다. 재진입 동기화가 없으면 동기화된 코드는 스레드 자체가 차단되는 것을 방지하기 위해 많은 추가 예방 조치를 취해야 합니다.

Atomic Access

In programming, an atomic action is one that effectively happens all at once. An atomic action cannot stop in the middle: it either happens completely, or it doesn't happen at all. No side effects of an atomic action are visible until the action is complete.

프로그래밍에서 atomic action은 한 번에 효과적으로 발생하는 동작입니다. atomic action은 중간에 멈출 수 없습니다. 완전히 일어나거나 전혀 일어나지 않습니다. 작업이 완료될 때까지 atomic action의 부작용이 표시되지 않습니다.

We have already seen that an increment expression, such as c++, does not describe an atomic action. Even very simple expressions can define complex actions that can decompose into other actions. However, there are actions you can specify that are atomic:

우리는 이미 C++와 같은 increment expression atomic action을 설명하지 않는다는 것을 보았습니다. 매우 간단한 표현식이라도 다른 작업으로 분해할 수 있는 복잡한 작업을 정의할 수 있습니다. 그러나 원자성으로 지정할 수 있는 작업이 있습니다.

  • Reads  writes는 참조 변수 및 대부분의 기본 변수(long 및 double을 제외한 모든 유형)에 대해 원자적입니다.
  • Reads  writes volatile 으로 선언된 모든 변수(long 및 double 변수 포함)에 대해 atomic 입니다.

Atomic actions cannot be interleaved, so they can be used without fear of thread interference. However, this does not eliminate all need to synchronize atomic actions, because memory consistency errors are still possible. Using volatile variables reduces the risk of memory consistency errors, because any write to a volatile variable establishes a happens-before relationship with subsequent reads of that same variable. This means that changes to a volatile variable are always visible to other threads. What's more, it also means that when a thread reads a volatile variable, it sees not just the latest change to the volatile, but also the side effects of the code that led up the change.

Atomic actions은 인터리브할 수 없으므로 스레드 간섭에 대한 두려움 없이 사용할 수 있습니다. 그러나 메모리 일관성 오류가 여전히 발생할 수 있기 때문에 원자적 작업을 동기화해야 하는 모든 필요성이 제거되지는 않습니다. volatile 변수를 사용하면 메모리 일관성 오류의 위험이 줄어듭니다. volatile 변수에 대한 모든 쓰기는 동일한 변수의 후속 읽기와 이전 발생(happens-before) 관계를 설정하기 때문입니다. 즉, volatile 변수에 대한 변경 사항은 항상 다른 스레드에서 볼 수 있습니다. 또한 스레드가 volatile 변수를 읽을 때 volatile에 대한 최신 변경 사항뿐만 아니라 변경을 이끈 코드의 부작용도 볼 수 있음을 의미합니다.

 

Using simple atomic variable access is more efficient than accessing these variables through synchronized code, but requires more care by the programmer to avoid memory consistency errors. Whether the extra effort is worthwhile depends on the size and complexity of the application.

Some of the classes in the java.util.concurrent package provide atomic methods that do not rely on synchronization. We'll discuss them in the section on High Level Concurrency Objects.

 

간단한 원자 변수 액세스를 사용하는 것이 동기화된 코드를 통해 이러한 변수에 액세스하는 것보다 더 효율적이지만 메모리 일관성 오류를 방지하려면 프로그래머가 더 많은 주의를 기울여야 합니다. 추가 노력이 가치가 있는지 여부는 응용 프로그램의 크기와 복잡성에 따라 다릅니다.

java.util.concurrent 패키지의 일부 클래스는 동기화에 의존하지 않는 원자성 메소드를 제공합니다. High Level Concurrency Objects 섹션에서 이에 대해 설명합니다.

'Java Tutorials' 카테고리의 다른 글

#25 Lesson: Introduction to Collections 1  (0) 2024.08.08
#24 Concurrency 2  (0) 2024.08.05
#22 Lesson: Exceptions  (0) 2024.08.05
#21 Lesson: Annotations  (0) 2024.08.02
#20 Lesson: Packages  (0) 2024.08.02