Как я создаю ориентированную на многопотоковое исполнение неперезаписываемую считанную много стоимость в Java?

Это - проблема, с которой я часто встречаюсь в работе с более сложными системами и которую я никогда не выяснял хороший способ решить. Это обычно включает вариации на тему общего объекта, конструкция которого и инициализация являются обязательно двумя отличными шагами. Это обычно из-за архитектурных требований, подобный апплетам, таким образом, ответы, которые предлагают, чтобы я консолидировал конструкцию и инициализацию, не полезны. Системы должны предназначаться для Java 4 самое позднее, так ответы, которые предлагают, поддержка, доступная только в позже JVMs, не полезны также.

Как пример, скажем, у меня есть класс, который структурирован для вписывания в среду разработки приложения как так:

public class MyClass
{

private /*ideally-final*/ SomeObject someObject;

MyClass() {
    someObject=null;
    }

public void startup() {
    someObject=new SomeObject(...arguments from environment which are not available until startup is called...);
    }

public void shutdown() {
    someObject=null; // this is not necessary, I am just expressing the intended scope of someObject explicitly
    }
}

Я не могу сделать someObject финал, так как он не может быть установлен, пока запуск () не вызывается. Но я действительно хотел бы, чтобы это отразило свою неперезаписываемую семантику и смогло непосредственно получить доступ к нему от нескольких потоков, предпочтительно избежав синхронизации.

Так как идея состоит в том, чтобы выразить и осуществлять степень окончательности, я предугадываю, что мог создать универсальный контейнер, как так (ОБНОВЛЕНИЕ - исправленная поточная обработка sematics этого класса):

public class WormRef<T>
{
private volatile T                      reference;                              // wrapped reference

public WormRef() {
    reference=null;
    }

public WormRef<T> init(T val) {
    if(reference!=null) { throw new IllegalStateException("The WormRef container is already initialized"); }
    reference=val;
    return this;
    }

public T get() {
    if(reference==null) { throw new IllegalStateException("The WormRef container is not initialized"); }
    return reference;
    }

}

и затем в MyClass, выше, сделайте:

private final WormRef<SomeObject> someObject;

MyClass() {
    someObject=new WormRef<SomeObject>();
    }

public void startup() {
    someObject.init(new SomeObject(...));
    }

public void sometimeLater() {
    someObject.get().doSomething();
    }

Который поднимает некоторые вопросы для меня:

  1. Существует ли лучший путь, или существующий объект Java (должно было бы быть доступным в Java 4)?

Во вторую очередь, с точки зрения потокобезопасности:

  1. Это ориентированное на многопотоковое исполнение при условии, что никакие другие доступы потока someObject.get() до окончания set() был назван. Другие потоки только вызовут методы на MyClass между запуском () и завершением работы () - платформа гарантирует это.
  2. Данный полностью несинхронизируемый WormReference контейнер, под любым JMM когда-либо возможно видеть значение object который не является ни пустым указателем, ни ссылкой на SomeObject? Другими словами, делает JMM, всегда гарантируют, что никакой поток не может наблюдать память объекта быть независимо от того, что значения, оказалось, были на "куче", когда объект был выделен. Я полагаю, что ответ - "Да", потому что выделение явно обнуляет выделенную память - но кэширование ЦП может привести к чему-то еще наблюдаемому в данной ячейке памяти?
  3. Действительно ли достаточно сделать WormRef.reference энергозависимый для обеспечения надлежащей многопоточной семантики?

Обратите внимание, что основная тяга этого вопроса состоит в том, как выразить и осуществить окончательность someObject не имея возможности на самом деле отметить его final; вторичный то, что необходимо для потокобезопасности. Таким образом, не становитесь слишком одержимыми аспектом потокобезопасности этого.

9
задан 11 revs, 2 users 89% 29 August 2014 в 07:09
поделиться

9 ответов

Это мой последний ответ, Реджис1 :

/**
 * Provides a simple write-one, read-many wrapper for an object reference for those situations
 * where you have an instance variable which you would like to declare as final but can't because
 * the instance initialization extends beyond construction.
 * <p>
 * An example would be <code>java.awt.Applet</code> with its constructor, <code>init()</code> and
 * <code>start()</code> methods.
 * <p>
 * Threading Design : [ ] Single Threaded  [x] Threadsafe  [ ] Immutable  [ ] Isolated
 *
 * @since           Build 2010.0311.1923
 */

public class WormRef<T>
extends Object
{

private volatile T                      reference;                              // wrapped reference

public WormRef() {
    super();

    reference=null;
    }

public WormRef<T> init(T val) {
    // Use synchronization to prevent a race-condition whereby the following interation could happen between three threads
    //
    //  Thread 1        Thread 2        Thread 3
    //  --------------- --------------- ---------------
    //  init-read null
    //                  init-read null
    //  init-write A
    //                                  get A
    //                  init-write B
    //                                  get B
    //
    // whereby Thread 3 sees A on the first get and B on subsequent gets.
    synchronized(this) {
        if(reference!=null) { throw new IllegalStateException("The WormRef container is already initialized"); }
        reference=val;
        }
    return this;
    }

public T get() {
    if(reference==null) { throw new IllegalStateException("The WormRef container is not initialized"); }
    return reference;
    }

} // END PUBLIC CLASS

(1) Отсылка к игровому шоу "Так вы хотите стать миллионером", которое вел Реджис Филберн.

0
ответ дан 3 November 2019 в 07:47
поделиться

Теоретически было бы достаточно переписать startup() следующим образом:

public synchronized void startup() {
    if (someObject == null) someObject = new SomeObject();
}

Кстати, хотя WoRmObject является конечным, потоки все еще могут вызывать set() несколько раз. Вам действительно нужно добавить синхронизацию.

update: Я немного поигрался с этим и создал SSCCE, возможно, вы найдете полезным немного поиграться с этим :)

package com.stackoverflow.q2428725;

import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Test {

    public static void main(String... args) throws Exception {
        Bean bean = new Bean();
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(4);
        executor.schedule(new StartupTask(bean), 2, TimeUnit.SECONDS);
        executor.schedule(new StartupTask(bean), 2, TimeUnit.SECONDS);
        Future<String> result1 = executor.submit(new GetTask(bean));
        Future<String> result2 = executor.submit(new GetTask(bean));
        System.out.println("Result1: " + result1.get());
        System.out.println("Result2: " + result2.get());
        executor.shutdown();
    }

}

class Bean {

    private String property;
    private CountDownLatch latch = new CountDownLatch(1);

    public synchronized void startup() {
        if (property == null) {
            System.out.println("Setting property.");
            property = "foo";
            latch.countDown();
        } else {
            System.out.println("Property already set!");
        }
    }   

    public String get() {
        try {
            latch.await();
        } catch (InterruptedException e) {
            // handle.
        }
        return property;
    }

}

class StartupTask implements Runnable {

    private Bean bean;

    public StartupTask(Bean bean) {
        this.bean = bean;
    }

    public void run() {
        System.out.println("Starting up bean...");
        bean.startup();
        System.out.println("Bean started!");
    }

}

class GetTask implements Callable<String> {

    private Bean bean;

    public GetTask(Bean bean) {
        this.bean = bean;
    }

    public String call() {
        System.out.println("Getting bean property...");
        String property = bean.get();
        System.out.println("Bean property got!");
        return property;
    }

}

Защелка CountDownLatch заставит все вызовы await() блокироваться, пока обратный отсчет не достигнет нуля.

1
ответ дан 3 November 2019 в 07:47
поделиться
  1. как насчет синхронизации?
  2. Нет, это не потокобезопасность. Без синхронизации новое состояние вашей переменной может никогда не быть передано другим потокам.
  3. Да, насколько мне известно, ссылки являются атомарными, поэтому вы увидите либо null, либо ссылку. Обратите внимание, что состояние указанного объекта - это совсем другая история
0
ответ дан 3 November 2019 в 07:47
поделиться

Я бы начал с объявления вашего someObject volatile .

private volatile SomeObject someObject;

Ключевое слово Volatile создает барьер памяти, что означает, что отдельные потоки всегда будут видеть обновленную память при обращении к someObject .

В вашей текущей реализации некоторые потоки могут по-прежнему видеть someObject как null даже после вызова startup .

На самом деле этот метод volatile часто используется коллекциями, объявленными в пакете java.util.concurrent .

И, как предлагают здесь некоторые другие плакаты, если все остальное не помогает, вернитесь к полной синхронизации.

1
ответ дан 3 November 2019 в 07:47
поделиться

Я бы удалил метод setter в WoRmObject и предоставил синхронизированный метод init(), который выдает исключение if (object != null)

1
ответ дан 3 November 2019 в 07:47
поделиться

Судя по вашему описанию фреймворка, это, скорее всего, потокобезопасность. Должен быть барьер памяти где-то между вызовом myobj.startup () и предоставлением myobj доступным для других потоков. Это гарантирует, что записи в startup () будут видны другим потокам. Поэтому вам не нужно беспокоиться о безопасности потоков, потому что это делает фреймворк. Хотя бесплатного обеда нет; Каждый раз, когда другой поток получает доступ к myobj через фреймворк, он должен включать синхронизацию или энергозависимое чтение.

Если вы заглянете в структуру и перечислите код в пути, вы увидите sync / volatile в нужных местах, которые сделают ваш код потокобезопасным. То есть, если фреймворк реализован правильно.

Давайте рассмотрим типичный пример свинга, когда рабочий поток выполняет некоторые вычисления, сохраняет результаты в глобальной переменной x , а затем отправляет событие перерисовки. Поток графического интерфейса пользователя после получения события перерисовки считывает результаты из глобальной переменной x и соответственно перерисовывает.

Ни рабочий поток, ни код перерисовки не выполняют синхронизацию или изменчивое чтение / запись чего-либо. Таких реализаций должны быть десятки тысяч. К счастью, все они потокобезопасны, хотя программисты не обращали на них особого внимания. Почему? Потому что очередь событий синхронизирована; у нас есть прекрасная цепочка «случай до того»:

write x - insert event - read event - read x

Следовательно, записи x и чтение x правильно синхронизируются, неявно через структуру событий.

1
ответ дан 3 November 2019 в 07:47
поделиться

Можете ли вы использовать ThreadLocal, который позволяет устанавливать значение каждого потока только один раз?

0
ответ дан 3 November 2019 в 07:47
поделиться

Есть много неправильных способов сделать ленивое инстанцирование, особенно в Java.

Короче говоря, наивный подход заключается в создании приватного объекта, публичного синхронизированного метода init и публичного несинхронизированного метода get, который выполняет проверку на нуль вашего объекта и вызывает init, если необходимо. Тонкости проблемы заключаются в выполнении проверки нуля потокобезопасным способом.

Эта статья может быть полезна: http://en.wikipedia.org/wiki/Double-checked_locking

Эта конкретная тема в Java подробно обсуждается в книге Дуга Ли "Concurrent Programming in Java", которая несколько устарела, и в "Java Concurrency in Practice", написанной в соавторстве с Ли и другими. В частности, CPJ был опубликован до выхода Java 5, которая значительно улучшила средства управления параллелизмом в Java.

Я могу опубликовать более конкретные сведения, когда вернусь домой и получу доступ к указанным книгам.

0
ответ дан 3 November 2019 в 07:47
поделиться

Рассмотрите возможность использования AtomicReference в качестве делегата в объекте-контейнере, который вы пытаетесь создать. Например:

public class Foo<Bar> {
private final AtomicReference<Bar> myBar = new AtomicReference<Bar>();
 public Bar get() {
  if (myBar.get()==null) myBar.compareAndSet(null,init());
  return myBar.get();
 }

 Bar init() { /* ... */ }
 //...
}

EDITED: Это будет установлено один раз, с каким-то ленивым методом инициализации. Это не идеально для блокирования нескольких вызовов (предположительно дорогих) init(), но могло быть и хуже. Вы можете засунуть инстанцирование myBar в конструктор, а затем позже добавить конструктор, позволяющий присваивать также, если предоставить правильную информацию.

Есть некоторое общее обсуждение потокобезопасного инстанцирования синглтонов (что довольно похоже на вашу проблему), например, на этом сайте.

1
ответ дан 3 November 2019 в 07:47
поделиться
Другие вопросы по тегам:

Похожие вопросы: