>  기사  >  Java  >  Java 동시 프로그래밍에서 객체의 공유 구현

Java 동시 프로그래밍에서 객체의 공유 구현

WBOY
WBOY앞으로
2023-04-23 17:25:071584검색

1. 가시성

일반적으로 읽기 작업을 수행하는 스레드가 다른 스레드가 쓴 값을 볼 수 있다고 보장할 수 없습니다. 각 스레드에는 고유한 캐싱 메커니즘이 있기 때문입니다. 여러 스레드 간의 메모리 쓰기 작업에 대한 가시성을 보장하려면 동기화 메커니즘을 사용해야 합니다.

public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        public void run() {
            while (!ready)
                Thread.yield();
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}

위 코드는 42를 출력하는 것처럼 보이지만 실제로는 전혀 종료되지 않을 수 있습니다. 왜냐하면 읽기 스레드가 Ready 값을 볼 수 없기 때문에 0을 출력할 가능성이 매우 높기 때문입니다. 이미 쓰여졌으나 나중에 숫자에 쓰여진 값이 보이지 않는 현상을 '재정렬'이라고 합니다. 동기화가 없으면 컴파일러, 프로세서, 런타임 등이 작업 실행 순서를 예기치 않게 조정할 수 있습니다.

따라서 여러 스레드 간에 데이터를 공유할 때마다 적절한 동기화를 사용해야 합니다.

1.1 잘못된 데이터

동기화를 사용하지 않으면 잘못된 변수 값을 얻을 가능성이 매우 높습니다. 유효하지 않은 값은 동시에 나타나지 않을 수 있으며 스레드는 한 변수의 최신 값과 다른 변수의 유효하지 않은 값을 얻을 수 있습니다. 유효하지 않은 데이터는 예상치 못한 예외, 손상된 데이터 구조, 부정확한 계산, 무한 루프 등과 같은 혼란스러운 오류로 이어질 수도 있습니다.

1.2 비원자적 64비트 작업

비휘발성 long 및 double 변수의 경우 JVM을 사용하면 64비트 읽기 또는 쓰기 작업을 두 개의 32비트 작업으로 분해할 수 있습니다. 따라서 최신 값의 상위 32비트와 유효하지 않은 값의 하위 32비트를 읽어서 임의의 값을 읽어올 가능성이 매우 높다. 휘발성 키워드로 선언되거나 잠금으로 보호되지 않는 한.

1.3 잠금 및 가시성

스레드가 잠금으로 보호되는 동기화된 코드 블록을 실행할 때 동일한 동기화된 코드 블록에서 다른 스레드의 모든 이전 작업 결과를 볼 수 있습니다. 동기화가 없으면 위의 보장을 달성할 수 없습니다. 잠금의 의미는 상호 배제 동작에만 국한되지 않고 가시성도 포함됩니다. 모든 스레드가 공유 변수의 최신 값을 볼 수 있도록 하려면 읽기 또는 쓰기 작업을 수행하는 모든 스레드가 동일한 잠금에서 동기화되어야 합니다.

1.4 휘발성 변수

변수가 휘발성 유형으로 선언되면 컴파일러나 런타임 모두 변수에 대한 작업 순서를 다른 메모리 작업과 함께 재정렬하지 않습니다. 휘발성 변수는 레지스터나 프로세서에 보이지 않는 다른 위치에 캐시되지 않으므로 휘발성 변수를 읽으면 항상 가장 최근에 작성된 값이 반환됩니다. 잠금 메커니즘은 가시성과 원자성을 모두 보장할 수 있는 반면, 휘발성 변수는 가시성만 보장할 수 있습니다.

휘발성 변수는 다음 조건이 모두 충족되는 경우에만 사용해야 합니다.

  • 변수에 대한 쓰기 작업은 변수의 현재 값에 의존하지 않거나 단일 스레드만 사용되도록 보장됩니다. 변수의 값을 업데이트하는 데 사용됩니다.

  • 이 변수는 다른 상태 변수와 함께 불변 조건에 포함되지 않습니다.

  • 변수에 액세스할 때 잠글 필요가 없습니다.

2. 릴리스 및 유출

객체를 게시한다는 것은 객체가 현재 범위 밖의 코드에서 사용될 수 있음을 의미합니다. 객체 게시 방법에는 비공개 변수에 대한 참조, 메서드 호출에서 반환된 참조, 내부 클래스 객체 게시 및 외부 클래스에 대한 암시적 참조 등이 포함됩니다. 해제되어서는 안 되는 객체가 해제되는 경우를 누출이라고 합니다.

public class ThisEscape {
   private int status;
   public ThisEscape(EventSource source) {
      source.registerListener(new EventListener() {
         public void onEvent(Event e) {
            doSomething(e);
         }
      });
      status = 1;
   }

   void doSomething(Event e) {
      status = e.getStatus();
   }

   interface EventSource {
      void registerListener(EventListener e);
   }

   interface EventListener {
      void onEvent(Event e);
   }

   interface Event {
      int getStatus();
   }
}

내부 클래스의 인스턴스에는 외부 클래스의 인스턴스에 대한 암시적 참조가 포함되어 있으므로 ThisEscape가 EventListener를 게시하면 암시적으로 ThisEscape 인스턴스 자체도 게시됩니다. 하지만 이때 변수 status가 초기화되지 않아 이 참조가 생성자에서 유출되는 문제가 발생했습니다. 잘못된 생성 프로세스를 피하기 위해 개인 생성자와 공용 팩토리 메서드를 사용할 수 있습니다.

public class SafeListener {
    private int status;
    private final EventListener listener;
    private SafeListener() {
        listener = new EventListener() {
            public void onEvent(Event e) {
                doSomething(e);
            }
        };
        status = 1;
    }
    public static SafeListener newInstance(EventSource source) {
        SafeListener safe = new SafeListener();
        source.registerListener(safe.listener);
        return safe;
    }

    void doSomething(Event e) {
        status = e.getStatus();
    }

    interface EventSource {
        void registerListener(EventListener e);
    }

    interface EventListener {
        void onEvent(Event e);
    }

    interface Event {
        int getStatus();
    }
}

3. 스레드 폐쇄

동기화 사용을 피하는 한 가지 방법은 공유하지 않는 것입니다. 단일 스레드 내에서만 데이터에 액세스하는 경우 스레드 폐쇄라고 하는 동기화가 필요하지 않습니다. 스레드 억제는 프로그래밍 고려 사항이며 프로그램에서 구현되어야 합니다. Java는 또한 로컬 변수 및 ThreadLocal과 같은 스레드 폐쇄를 유지하는 데 도움이 되는 몇 가지 메커니즘을 제공합니다.

3.1 임시 스레드 폐쇄

임시 스레드 폐쇄는 스레드 폐쇄를 유지하는 책임이 전적으로 프로그램 구현에 있다는 것을 의미합니다. 휘발성 변수를 사용하는 것은 임시 스레드 폐쇄를 달성하는 방법입니다. 단일 스레드만 공유 휘발성 변수에 대한 쓰기 작업을 수행하는 것이 보장되는 한 이러한 변수에 대해 "읽기-수정-쓰기" 작업을 수행하는 것이 안전합니다. . 휘발성 변수의 가시성을 통해 다른 스레드가 최신 값을 볼 수 있습니다.

임시 스레드 폐쇄는 매우 취약하므로 프로그램에서 가능한 한 적게 사용하십시오. 가능하다면 스택 억제 및 ThreadLocal과 같은 다른 스레드 억제 기술을 사용하십시오.

3.2 스택 클로저

스택 클로저에서는 지역 변수를 통해서만 객체에 접근할 수 있습니다. 실행 스레드의 스택에 위치하며 다른 스레드에서 액세스할 수 없습니다. 이러한 개체는 스레드로부터 안전하지 않더라도 여전히 스레드로부터 안전합니다. 그러나 코드를 작성하는 사람만이 어떤 개체가 스택에 포함되어 있는지 알 수 있다는 점은 주목할 가치가 있습니다. 명확한 지침이 없으면 후속 유지 관리 담당자가 실수로 이러한 개체를 쉽게 유출할 수 있습니다.

3.3 ThreadLocal类

使用ThreadLocal是一种更规范的线程封闭方式,它能是线程中的某个值与保存值的对象关联起来。如下代码,通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接:

public class ConnectionDispenser {
    static String DB_URL = "jdbc:mysql://localhost/mydatabase";

    private ThreadLocal<Connection> connectionHolder
        = new ThreadLocal<Connection>() {
            public Connection initialValue() {
                try {
                    return DriverManager.getConnection(DB_URL);
                } catch (SQLException e) {
                    throw new RuntimeException("Unable to acquire Connection, e");
                }
        };
    };

    public Connection getConnection() {
        return connectionHolder.get();
    }
}

从概念上看,你可以将ThreadLocal8742468051c85b06f0a0af9e3e506b5c视为包含了Mapdd13f7ef6939263b34f16ddd764e4ff9对象,其中保存了特定于改线程的值,但ThreadLocal的实现并非如此。这些特定于线程的值保存在Thread对象中,当线程终止后,这些值会作为垃圾被回收。

4. 不变性

如果某个对象在被创建后其状态就不能被修改,那么这个对象就被称为不可变对象。满足同步需求的另一种方法就是使用不可变对象。不可变对象一定是线程安全的。当满足以下条件时,对象才是不可变的:

  • 对象创建以后其状态就不能改变

  • 对象的所有域都是final类型

  • 对象是正确创建的,在对象创建期间,this引用没有泄露

public final class ThreeStooges {
    private final Set<String> stooges = new HashSet<String>();

    public ThreeStooges() {
        stooges.add("Moe");
        stooges.add("Larry");
        stooges.add("Curly");
    }

    public boolean isStooge(String name) {
        return stooges.contains(name);
    }
}

上述代码中,尽管stooges对象是可变的,但在它构造完成后无法对其修改。stooges是一个final类型的引用变量,因此所有的对象状态都通过一个final域访问。在构造函数中,this引用不能被除了构造函数之外的代码访问到。

4.1 final域

final类型的域是不能修改的,但如果final域所引用的对象是可变的,那么这些被引用的对象是可以修改的。final域的对象在构造函数中不会被重排序,所以final域也能保证初始化过程的安全性。和“除非需要更高的可见性,否则应将所有的域都声明为私用域”一样,“除非需要某个域是可变的,否则应将其声明为final域”也是一个良好的编程习惯。

4.2 使用volatile类型来发布不可变对象

因式分解Sevlet将执行两个原子操作:

  • 更新缓存

  • 通过判断缓存中的数值是否等于请求的数值来决定是否直接读取缓存中的结果

每当需要一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据:

public class OneValueCache {
    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;

    public OneValueCache(BigInteger i, BigInteger[] factors) {
        lastNumber = i;
        lastFactors = Arrays.copyOf(factors, factors.length);
    }

    public BigInteger[] getFactors(BigInteger i) {
        if (lastNumber == null || !lastNumber.equals(i))
            return null;
        else
            return Arrays.copyOf(lastFactors, lastFactors.length);
    }
}

当线程获取了不可变对象的引用后,不必担心另一个线程会修改对象的状态。如果要更新这些变量,可以创建一个新的容器对象,但其他使用原有对象的线程仍然会看到对象处于一致的状态。当一个线程将volatile类型的cache设置为引用一个新的OneValueCache时,其他线程就会立即看到新缓存的数据:

public class VolatileCachedFactorizer implements Servlet {
    private volatile OneValueCache cache = new OneValueCache(null, null);

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = cache.getFactors(i);
        if (factors == null) {
            factors = factor(i);
            cache = new OneValueCache(i, factors);
        }
        encodeIntoResponse(resp, factors);
    }
}

5 安全发布

5.1 不正确的发布

像这样将对象引用保存到公有域中就是不安全的:

public Holder holder;
public void initialize(){
    holder = new Holder(42);
}

由于存在可见性问题,其他线程看到的Holder对象将处于不一致的状态。除了发布对象的线程外,其他线程可以看到Holder域是一个失效值,因此将看到一个空引用或者之前的旧值。

public class Holder {
    private int n;

    public Holder(int n) {
        this.n = n;
    }

    public void assertSanity() {
        if (n != n)
            throw new AssertionError("This statement is false.");
    }
}

上述代码,即使Holder对象被正确的发布,assertSanity也有可能抛出AssertionError。因为线程看到Holder引用的值是最新的,但由于重排序Holder状态的值却是时效的。

5.2 不可变对象与初始化安全性

即使在发布不可变对象的引用时没有使用同步,也仍然可以安全地访问该对象。任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。在没有额外同步的情况下,也可以安全地访问final类型的域。然而,如果final类型的域所指向的是可变对象,那么在访问这些域所指向的对象的状态时仍然需要同步。

5.3 安全发布的常用模式

要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全发布:

  • 在静态初始化函数里初始化一个对象引用。

  • 将对象的引用保存到volatile类型的域或者AtomicReference对象中。

  • 将对象的引用保存到某个正确构造对象的final类型域中。

  • 将对象的引用保存到一个由锁保护的域中。

线程安全库中的容器类提供了以下的安全发布保证:

  • 通过将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程。

  • 通过将某个对象放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或者synchronizedSet中,可以将该对象安全地发布到任何从这些容器中访问该对象的线程。

  • 객체를 BlockingQueue 또는 ConcurrentLinkedQueue에 넣으면 이러한 큐에서 객체에 액세스하는 모든 스레드에 객체를 안전하게 게시할 수 있습니다.

5.4 실제로 불변 객체

객체가 기술적으로 변경 가능하지만 해제된 후에도 상태가 변경되지 않는 경우 이러한 객체를 사실상 불변 객체라고 합니다. 안전하게 게시된 사실상 불변 개체는 추가 동기화 없이 모든 스레드에서 안전하게 사용할 수 있습니다. 예를 들어, 각 사용자의 최신 로그인 시간을 저장하는 Map 개체를 유지 관리합니다.

public Map7c36a245a97922f78d3224cbec5818e1 lastLogin =
Collections.synchronizedMap(new HashMapDate 값 객체는 맵에 넣은 후에는 변경되지 않으므로, 동기화된 맵의 동기화 메커니즘은 날짜 값을 안전하게 게시할 수 있을 만큼 충분하며 이러한 날짜 값에 액세스할 때 추가 동기화가 필요하지 않습니다.

5.5 변경 가능한 개체

변경 가능한 개체의 경우 개체를 게시할 때 동기화가 필요할 뿐만 아니라 후속 수정 작업의 가시성을 보장하기 위해 개체에 액세스할 때마다 동기화를 사용해야 합니다. 개체의 게시 요구 사항은 변경 가능성에 따라 다릅니다.

  • 불변 개체는 모든 메커니즘으로 게시될 수 있습니다.

  • 사실 불변 객체는 안전한 방법으로 게시되어야 합니다.

  • 변경 가능한 객체는 안전한 방법으로 해제되어야 하며 스레드로부터 안전하거나 잠금으로 보호되어야 합니다.

5.6 안전한 공유 객체

다음을 포함하여 동시 프로그램에서 객체를 사용하고 공유할 때 사용할 수 있는 몇 가지 실용적인 전략이 있습니다.

  • 스레드 제한. 스레드로 둘러싸인 개체는 하나의 스레드만 소유할 수 있고 개체는 해당 스레드에 포함되어 있으며 이 스레드에 의해서만 수정될 수 있습니다.

  • 읽기 전용 공유. 추가 동기화 없이 공유 읽기 전용 개체는 여러 스레드에서 동시에 액세스할 수 있지만 어떤 스레드도 이를 수정할 수 없습니다. 공유된 읽기 전용 객체에는 불변 객체와 사실상 불변 객체가 포함됩니다.

  • 스레드로부터 안전한 공유. 스레드로부터 안전한 개체는 내부적으로 동기화되므로 여러 스레드가 추가 동기화 없이 개체의 공용 인터페이스를 통해 액세스할 수 있습니다.

  • 개체를 보호하세요. 보호된 개체는 특정 잠금을 보유해야만 액세스할 수 있습니다. 보호되는 객체에는 다른 스레드로부터 안전한 객체에 캡슐화된 객체뿐만 아니라 특정 잠금으로 해제되고 보호되는 객체도 포함됩니다.

위 내용은 Java 동시 프로그래밍에서 객체의 공유 구현의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제