본문 바로가기
Java Tutorials

#24 Concurrency 2

by xogns93 2024. 8. 5.

자바 공식 Concurrency 튜토리얼

 

Liveness

적시에 실행할 수 있는 동시 응용 프로그램의 기능을 활성 상태(liveness)라고 합니다. 이 섹션에서는 가장 일반적인 종류의 활동성(liveness) 문제인 교착 상태(deadlock)에 대해 설명하고 계속해서 다른 두 가지 활동성 문제인 기아 및 라이브락(starvation and livelock)에 대해 간략하게 설명합니다.

 

Deadlock

교착 상태(Deadlock)는 둘 이상의 스레드가 서로를 기다리며 영원히 차단되는 상황을 설명합니다. 여기에 예가 있습니다.

Alphonse와 Gaston은 친구이며 예의를 중요시합니다. 친구에게 인사를 할 때 친구가 인사에 대한 응답을 할 때까지 인사를 하고 있어야 한다는 엄격한 예절이 있습니다. 불행하게도, 이 규칙은 두 친구가 동시에 서로 인사할 가능성을 설명하지 않습니다. 이 예제 애플리케이션 Deadlock은 다음 가능성을 모델링합니다.

 

public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s"
                + "  has bowed to me!%n", 
                this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s"
                + " has bowed back to me!%n",
                this.name, bower.getName());
        }
    }

    public static void main(String[] args) {
        final Friend alphonse =
            new Friend("Alphonse");
        final Friend gaston =
            new Friend("Gaston");
        new Thread(new Runnable() {
            public void run() { alphonse.bow(gaston); }
        }).start();
        new Thread(new Runnable() {
            public void run() { gaston.bow(alphonse); }
        }).start();
    }
}
 

데드락이 실행될 때, 두 스레드가 모두 bowBack을 호출하려고 할 때 블록될 확률이 매우 높습니다. 각 스레드는 서로가 bow를 종료할 때까지 대기하고 있기 때문에 어느 블록도 종료되지 않을 것입니다. 이는 각 스레드가 서로의 작업이 끝나기를 기다리며, 무한히 블록되는 상황을 만들어내는 것입니다. 이러한 상태를 데드락이라고 합니다.

 

Starvation and Livelock

기아 상태(Starvation)와 라이브락(livelock)은 교착 상태(deadlock)보다 훨씬 덜 일반적인 문제이지만 동시성 소프트웨어의 모든 설계자가 여전히 직면할 가능성이 있는 문제입니다.

 

Starvation

Starvation는 스레드가 특정 공유 리소스에 정규적인 액세스를 얻지 못하고 작업을 잘 진행할 수 없는 상황을 말합니다. 이는 "탐욕스러운(greedy)" 스레드들에 의해 공유 리소스가 장기간 사용 불가능한 상태로 만들어지는 경우 발생합니다. 예를 들어, 한 객체가 자주 메서드 리턴에 오랜 시간이 걸리는 동기화된 메서드를 제공한다고 가정해봅시다. 하나의 스레드가 이 메서드를 자주 호출하는 경우, 동일한 객체에 자주 동기화된 액세스가 필요한 다른 스레드들은 종종 블록(block)될 수 있습니다. 이러한 상황에서는 일부 스레드가 지나치게 우선권을 가지고 리소스를 점유하여 다른 스레드들이 자원에 접근하지 못하고 진전을 이룰 수 없게 됩니다.

 

Livelock

스레드는 종종 다른 스레드의 작업에 대한 응답으로 동작합니다. 다른 스레드의 작업이 다른 스레드의 작업에 대한 응답이기도 하면 livelock이 발생할 수 있습니다. 교착 상태와 마찬가지로 활성 상태인 스레드는 더 이상 진행할 수 없습니다. 그러나 스레드는 차단되지 않습니다. 단순히 작업을 재개하기에는 서로 응답하느라 너무 바쁩니다. 이것은 복도에서 두 사람이 서로를 지나치려고 시도하는 것과 비슷합니다. Alphonse는 Gaston이 통과하도록 왼쪽으로 이동하고 Gaston은 Alphonse가 통과하도록 오른쪽으로 이동합니다. 그들이 여전히 서로를 막고 있는 것을 보고 Alphone은 오른쪽으로 이동하고 Gaston은 왼쪽으로 이동합니다. 아직도 서로 막고 있어서...

 

Guarded Blocks

스레드는 종종 서로의 작업을 조정해야 합니다. 가장 일반적인 조정 관용구는 "보호된 블록(guarded block)"입니다. 이러한 블록은 블록이 진행되기 전에 True이어야 하는 조건을 폴링(polling)합니다. 이를 올바르게 수행하기 위해 몇 가지 단계를 따라야 합니다.

예를 들어, guardedJoy가 다른 스레드에 의해 set되기 전까지 진행되어서는 안 되는 메서드라고 가정해봅시다. 이러한 메서드는 이론적으로 조건이 충족될 때까지 단순히 루프를 반복할 수 있지만, 이 루프는 낭비적입니다. 왜냐하면 기다리는 동안 계속해서 실행되기 때문입니다.

public void guardedJoy() {
    // Simple loop guard. Wastes
    // processor time. Don't do this!
    while(!joy) {}
    System.out.println("Joy has been achieved!");
}
 

더 효율적인 가드는 Object.wait을 호출하여 현재 스레드를 일시 정지시킵니다. wait의 호출은 다른 스레드가 특정 이벤트가 발생했다는 통지를 공지할 때까지 리턴하지 않습니다. 이때, 이 통지는 이 스레드가 대기 중인 이벤트와 반드시 일치하는 것은 아닙니다.

public synchronized void guardedJoy() {
    // This guard only loops once for each special event, which may not
    // be the event we're waiting for.
    while(!joy) {
        try {
            wait();
        } catch (InterruptedException e) {}
    }
    System.out.println("Joy and efficiency have been achieved!");
}
 
Note: 
wait를 호출할 때는 항상 대기 중인 조건을 테스트하는 루프 내에서 호출해야 합니다. 대기 중인 특정 조건에 대한 인터럽트가 발생했다고 가정하거나 해당 조건이 여전히 True인 것으로 가정해서는 안 됩니다.

wait와 같이 실행을 일시 중단(suspend)하는 많은 메서드는 InterruptedException을 throw할 수 있습니다. 이 예제에서는 해당 예외를 무시할 수 있습니다. 우리는 joy의 가치를 중요시하기 때문입니다.

 

Why is this version of guardedJoy synchronized? Suppose d is the object we're using to invoke wait. When a thread invokes d.wait, it must own the intrinsic lock for d — otherwise an error is thrown. Invoking wait inside a synchronized method is a simple way to acquire the intrinsic lock.

이 버전의 guardedJoy가 동기화되는 이유는 무엇일까요? d가 wait를 호출하는 데 사용하는 객체라고 가정합니다. 스레드가 d.wait를 호출하면 d에 대한 고유(intrinsic) 락을 소유해야 합니다. 그렇지 않으면 오류가 발생합니다. synchronized 메서드 내에서 wait를 호출하는 것은 고유 락을 획득하는 간단한 방법입니다.

 

wait가 호출되면 스레드는 락을 해제하고 실행을 일시 중단(suspend)합니다. 나중에 다른 스레드가 동일한 락을 획득하고 Object.notifyAll을 호출하여 해당 락을 기다리고 있는 모든 스레드에게 중요한 것이 발생했음을 알립니다.

public synchronized notifyJoy() {
    joy = true;
    notifyAll();
}
 

두 번째 스레드가 락을 해제한 후에, 어느 시점 이후에 첫 번째 스레드는 락을 다시 획득하고, wait를 호출로부터 리턴하여 계속 진행합니다.

Note:notify라는 또 다른 통지 메서드도 있으며, 이 메서드는 단일 스레드를 깨웁니다. notify는 깨워질 스레드를 지정할 수 없기 때문에 대규모 병렬 애플리케이션에서만 유용합니다. 즉, 많은 수의 스레드가 유사한 작업을 수행하는 프로그램에서 유용합니다. 이러한 애플리케이션에서는 어떤 스레드가 깨어나는지 신경 쓰지 않습니다.

 

가드된 블록을 사용하여 생산자-소비자(Producer-Consumer) 애플리케이션을 만들어 봅시다. 이러한 종류의 애플리케이션은 두 개의 스레드 간에 데이터를 공유합니다. 생산자는 데이터를 생성하고, 소비자는 그 데이터를 이용하는 역할을 합니다. 이 두 개의 스레드는 공유 객체를 사용하여 통신합니다. 조정은 필수적입니다: 소비자 스레드는 생산자 스레드가 데이터를 전달하기 전에 데이터를 검색하려 시도해서는 안 되며, 생산자 스레드는 이전 데이터가 소비자에게 전달되기 전에 새로운 데이터를 전달하려고 시도해서는 안 됩니다.

 

이 예제에서 데이터는 텍스트 메시지의 시리즈이며, 이 데이터는 Drop이라는 타입의 객체를 통해 공유됩니다.

public class Drop {
    // Message sent from producer
    // to consumer.
    private String message;
    // True if consumer should wait
    // for producer to send message,
    // false if producer should wait for
    // consumer to retrieve message.
    private boolean empty = true;

    public synchronized String take() {
        // Wait until message is
        // available.
        while (empty) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        // Toggle status.
        empty = true;
        // Notify producer that
        // status has changed.
        notifyAll();
        return message;
    }

    public synchronized void put(String message) {
        // Wait until message has
        // been retrieved.
        while (!empty) {
            try { 
                wait();
            } catch (InterruptedException e) {}
        }
        // Toggle status.
        empty = false;
        // Store message.
        this.message = message;
        // Notify consumer that status
        // has changed.
        notifyAll();
    }
}
 

Producer에 정의된 생산자 스레드는 일련의 친숙한 메시지를 보냅니다. 문자열 "DONE"은 모든 메시지가 전송되었음을 나타냅니다. 실제 응용 프로그램의 예측할 수 없는 특성을 시뮬레이트하기 위해 생산자 스레드는 메시지 사이의 임의 간격 동안 일시 중지됩니다.

import java.util.Random;

public class Producer implements Runnable {
    private Drop drop;

    public Producer(Drop drop) {
        this.drop = drop;
    }

    public void run() {
        String importantInfo[] = {
            "Mares eat oats",
            "Does eat oats",
            "Little lambs eat ivy",
            "A kid will eat ivy too"
        };
        Random random = new Random();

        for (int i = 0;
             i < importantInfo.length;
             i++) {
            drop.put(importantInfo[i]);
            try {
                Thread.sleep(random.nextInt(5000));
            } catch (InterruptedException e) {}
        }
        drop.put("DONE");
    }
}
 

Consumer에서 정의된 소비자 스레드는 단순히 메시지를 검색하고 출력합니다. "DONE" 문자열을 검색할 때까지 이 작업을 반복합니다. 이 스레드도 임의의 시간 간격 동안 일시 정지합니다.

import java.util.Random;

public class Consumer implements Runnable {
    private Drop drop;

    public Consumer(Drop drop) {
        this.drop = drop;
    }

    public void run() {
        Random random = new Random();
        for (String message = drop.take();
             ! message.equals("DONE");
             message = drop.take()) {
            System.out.format("MESSAGE RECEIVED: %s%n", message);
            try {
                Thread.sleep(random.nextInt(5000));
            } catch (InterruptedException e) {}
        }
    }
}
 

마지막으로, ProducerConsumerExample에서 정의된 main 스레드는 생산자와 소비자 스레드를 실행합니다.

public class ProducerConsumerExample {
    public static void main(String[] args) {
        Drop drop = new Drop();
        (new Thread(new Producer(drop))).start();
        (new Thread(new Consumer(drop))).start();
    }
}
 

Note: The Drop class was written in order to demonstrate guarded blocks. To avoid re-inventing the wheel, examine the existing data structures in the Java Collections Framework before trying to code your own data-sharing objects. For more information, refer to the Questions and Exercises section.

Drop 클래스는 가드된 블록을 보여주기 위해 작성되었습니다. 바퀴를 다시 발명하지 않기 위해, 자신만의 데이터 공유 객체를 작성하기 전에 Java Collections Framework의 기존 데이터 구조를 살펴보세요. 더 많은 정보는 "Questions and Exercises" 섹션을 참조하세요.

 

Immutable Objects

An object is considered immutable if its state cannot change after it is constructed. Maximum reliance on immutable objects is widely accepted as a sound strategy for creating simple, reliable code.

객체가 생성된 후에 상태가 변경되지 않는다면 해당 객체는 불변(immutable) 객체로 간주됩니다. 불변 객체에 대한 최대한의 의존은 단순하고 신뢰성 있는 코드를 작성하기 위한 효과적인 전략으로 널리 인정받고 있습니다.

Immutable objects are particularly useful in concurrent applications. Since they cannot change state, they cannot be corrupted by thread interference or observed in an inconsistent state.

불변(Immutable) 객체는 특히 동시성 애플리케이션에서 유용합니다. 상태를 변경할 수 없기 때문에 스레드간 간섭으로 인해 손상될 수도 없으며, 일관되지 않은 상태로 관찰될 수도 없습니다.

Programmers are often reluctant to employ immutable objects, because they worry about the cost of creating a new object as opposed to updating an object in place. The impact of object creation is often overestimated, and can be offset by some of the efficiencies associated with immutable objects. These include decreased overhead due to garbage collection, and the elimination of code needed to protect mutable objects from corruption.

프로그래머들은 종종 객체를 업데이트하는 대신 새로운 객체를 생성하는 비용에 대해 우려를 표합니다. 객체 생성의 영향은 종종 과대평가되며, 불변 객체와 관련된 몇 가지 효율성으로 상쇄될 수 있습니다. 이러한 효율성에는 가비지 수집으로 인한 오버헤드 감소와, 가변 객체를 손상으로부터 보호하기 위해 필요한 코드의 제거 등이 포함됩니다.

 

The following subsections take a class whose instances are mutable and derives a class with immutable instances from it. In so doing, they give general rules for this kind of conversion and demonstrate some of the advantages of immutable objects.

다음 하위 섹션에서는 인스턴스가 변경 가능한 클래스를 가져오고 변경 불가능한 인스턴스가 있는 클래스를 파생시킵니다. 그렇게 함으로써 그들은 이러한 종류의 변환에 대한 일반적인 규칙을 제시하고 불변 객체의 장점 중 일부를 보여줍니다.

 

A Synchronized Class Example

The class, SynchronizedRGB, defines objects that represent colors. Each object represents the color as three integers that stand for primary color values and a string that gives the name of the color.

SynchronizedRGB 클래스는 색상을 나타내는 객체를 정의합니다. 각 객체는 기본 색상 값을 나타내는 세 개의 정수와 색상 이름을 제공하는 문자열로 색상을 나타냅니다.

public class SynchronizedRGB {

    // Values must be between 0 and 255.
    private int red;
    private int green;
    private int blue;
    private String name;

    private void check(int red,
                       int green,
                       int blue) {
        if (red < 0 || red > 255
            || green < 0 || green > 255
            || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }

    public SynchronizedRGB(int red,
                           int green,
                           int blue,
                           String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }

    public void set(int red,
                    int green,
                    int blue,
                    String name) {
        check(red, green, blue);
        synchronized (this) {
            this.red = red;
            this.green = green;
            this.blue = blue;
            this.name = name;
        }
    }

    public synchronized int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }

    public synchronized String getName() {
        return name;
    }

    public synchronized void invert() {
        red = 255 - red;
        green = 255 - green;
        blue = 255 - blue;
        name = "Inverse of " + name;
    }
}
 

SynchronizedRGB must be used carefully to avoid being seen in an inconsistent state. Suppose, for example, a thread executes the following code:

SynchronizedRGB는 일관성 없는 상태로 표시되지 않도록 주의해서 사용해야 합니다. 예를 들어 스레드가 다음 코드를 실행한다고 가정합니다.

SynchronizedRGB color =
    new SynchronizedRGB(0, 0, 0, "Pitch Black");
...
int myColorInt = color.getRGB();      //Statement 1
String myColorName = color.getName(); //Statement 2
 

If another thread invokes color.set after Statement 1 but before Statement 2, the value of myColorInt won't match the value of myColorName. To avoid this outcome, the two statements must be bound together:

다른 스레드가 Statement 1 이후와 Statement 2 이전에 color.set을 호출하는 경우 myColorInt 값은 myColorName 값과 일치하지 않습니다. 이 결과를 방지하려면 두 statements을 하나로 묶어야 합니다.

synchronized (color) {
    int myColorInt = color.getRGB();
    String myColorName = color.getName();
}
 

This kind of inconsistency is only possible for mutable objects — it will not be an issue for the immutable version of SynchronizedRGB.

이러한 종류의 불일치는 변경 가능한 객체에서만 가능합니다. 변경 불가능한 버전의 SynchronizedRGB에서는 문제가 되지 않습니다.

A Strategy for Defining Immutable Objects

The following rules define a simple strategy for creating immutable objects. Not all classes documented as "immutable" follow these rules. This does not necessarily mean the creators of these classes were sloppy — they may have good reason for believing that instances of their classes never change after construction. However, such strategies require sophisticated analysis and are not for beginners.

다음 규칙은 변경할 수 없는 객체를 만들기 위한 간단한 전략을 정의합니다. "불변"으로 문서화된 모든 클래스가 이러한 규칙을 따르는 것은 아닙니다. 이것은 반드시 이러한 클래스의 생성자가 엉성했다는 것을 의미하지는 않습니다. 그들은 클래스의 인스턴스가 생성 후에 절대 변경되지 않는다고 믿을 충분한 이유가 있을 수 있습니다. 그러나 이러한 전략은 정교한 분석이 필요하며 초보자에게는 적합하지 않습니다.

 

  1. 필드 또는 필드가 참조하는 개체를 수정하는 메서드인 "setter" 메서드를 제공하지 마십시오.
  2. 모든 필드를 최종적이고 비공개로 만드십시오.
  3. 하위 클래스가 메서드를 재정의하도록 허용하지 마십시오. 이를 수행하는 가장 간단한 방법은 클래스를 final로 선언하는 것입니다. 보다 정교한 접근 방식은 생성자를 비공개로 만들고 팩터리 메서드에서 인스턴스를 생성하는 것입니다.
  4. 인스턴스 필드에 변경 가능한 개체에 대한 참조가 포함되어 있으면 해당 개체를 변경할 수 없습니다:
    • Don't provide methods that modify the mutable objects.
    • Don't share references to the mutable objects. Never store references to external, mutable objects passed to the constructor; if necessary, create copies, and store references to the copies. Similarly, create copies of your internal mutable objects when necessary to avoid returning the originals in your methods.

이 전략을 SynchronizedRGB에 적용하면 다음 단계가 수행됩니다:

  1. There are two setter methods in this class. The first one, set, arbitrarily transforms the object, and has no place in an immutable version of the class. The second one, invert, can be adapted by having it create a new object instead of modifying the existing one.
  2. All fields are already private; they are further qualified as final.
  3. The class itself is declared final.
  4. Only one field refers to an object, and that object is itself immutable. Therefore, no safeguards against changing the state of "contained" mutable objects are necessary.

After these changes, we have ImmutableRGB:

final public class ImmutableRGB {

    // Values must be between 0 and 255.
    final private int red;
    final private int green;
    final private int blue;
    final private String name;

    private void check(int red,
                       int green,
                       int blue) {
        if (red < 0 || red > 255
            || green < 0 || green > 255
            || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }

    public ImmutableRGB(int red,
                        int green,
                        int blue,
                        String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }


    public int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }

    public String getName() {
        return name;
    }

    public ImmutableRGB invert() {
        return new ImmutableRGB(255 - red,
                       255 - green,
                       255 - blue,
                       "Inverse of " + name);
    }
}
 

 

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

#26 Lesson: Introduction to Collections 2  (0) 2024.08.11
#25 Lesson: Introduction to Collections 1  (0) 2024.08.08
#23 Concurrency 1  (0) 2024.08.05
#22 Lesson: Exceptions  (0) 2024.08.05
#21 Lesson: Annotations  (0) 2024.08.02