Доказательство следующего кода, не ориентированного на многопотоковое исполнение

Как я могу быстро доказать, что следующий класс не ориентирован на многопотоковое исполнение (поскольку он использует Ленивую Инициализацию и не использование синхронизации) путем написания некоторого кода? Другими словами, если я тестирую следующий класс на потокобезопасность, как я могу привести его к сбою?

public class LazyInitRace {
  private ExpensiveObject instance = null;

  public ExpensiveObject getInstance() {
     if (instance == null)
        instance = new ExpensiveObject();
    return instance;
  }
}
12
задан Don Ch 9 March 2010 в 16:12
поделиться

8 ответов

Что ж ... Результатом этого кода будет ложь, тогда как вы ожидаете истины.

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class LazyInitRace {

    public class ExpensiveObject {
        public ExpensiveObject() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
        }
    }

    private ExpensiveObject instance = null;

    public ExpensiveObject getInstance() {
        if (instance == null)
            instance = new ExpensiveObject();
        return instance;
    }

    public static void main(String[] args) {
        final LazyInitRace lazyInitRace = new LazyInitRace();

        FutureTask<ExpensiveObject> target1 = new FutureTask<ExpensiveObject>(
                new Callable<ExpensiveObject>() {

                    @Override
                    public ExpensiveObject call() throws Exception {
                        return lazyInitRace.getInstance();
                    }
                });
        new Thread(target1).start();

        FutureTask<ExpensiveObject> target2 = new FutureTask<ExpensiveObject>(
                new Callable<ExpensiveObject>() {

                    @Override
                    public ExpensiveObject call() throws Exception {
                        return lazyInitRace.getInstance();
                    }
                });
        new Thread(target2).start();

        try {
            System.out.println(target1.get() == target2.get());
        } catch (InterruptedException e) {
        } catch (ExecutionException e) {
        }
    }
}
2
ответ дан 2 December 2019 в 04:02
поделиться

По определению, условия гонки не могут быть протестированы детерминированно, если вы не управляете планировщиком потоков (чего вы не делаете). Самое близкое, что вы можете сделать, - это либо добавить настраиваемую задержку в метод getInstance () , либо написать код, в котором проблема может проявиться, и запустить ее тысячи раз в цикле.

Кстати, ничто из этого на самом деле не является «доказательством». Формальная проверка могла бы, но это очень, очень сложно сделать даже для относительно небольшого количества кода.

15
ответ дан 2 December 2019 в 04:02
поделиться

Что ж, это не потокобезопасность. Проверка потоковой безопасности случайна, но довольно проста:

  1. Сделать дорогой объект конструктор полностью безопасен:

    synchronized ExhibitedObject () {...

  2. Поместите в код конструктора, который проверяет, существует ли другая копия объекта, а затем вызывает исключение.

  3. Создать поточно-безопасный метод для очистки переменной экземпляра

  4. Поместить последовательный код getInstance / clearInstance в цикл для выполнения несколькими потоками и дождаться исключения из (2)

-1
ответ дан 2 December 2019 в 04:02
поделиться

Это легко проверить с помощью отладчика.

  1. Напишите программу, которая вызывает getInstance () в двух отдельных потоках.
  2. Установите точку останова на конструкции ExrivateObject. Убедитесь, что отладчик приостанавливает только поток , а не виртуальную машину.
  3. Затем, когда первый поток останавливается на точке останова, оставьте его приостановленным.
  4. Когда второй поток останавливается, вы просто продолжаете.
  5. Если вы проверите результат вызова getInstance () для обоих потоков, они будут ссылаться на разные экземпляры .

Преимущество этого способа состоит в том, что вам фактически не нужен ExpendanceObject, любой объект фактически даст те же результаты. Вы просто используете отладчик для планирования выполнения этой конкретной строки кода и, таким образом, создания детерминированного результата.

0
ответ дан 2 December 2019 в 04:02
поделиться

Поместите в конструктор действительно длинное вычисление:

public ExpensiveObject()
{
    for(double i = 0.0; i < Double.MAX_VALUE; ++i)
    {
        Math.pow(2.0,i);
    }
}

Вы можете уменьшить условие завершения до Double.MAX_VALUE / 2.0 или разделить на большее число, если MAX_VALUE равно занимает слишком много времени по вашему вкусу.

0
ответ дан 2 December 2019 в 04:02
поделиться

Можете ли вы заставить ExpensiveObject долго конструироваться в вашем тесте? Если да, то просто вызовите getInstance() дважды из двух разных потоков за достаточно короткое время, чтобы первый конструктор не успел завершиться до второго вызова. В итоге будет создано два разных экземпляра, что и должно привести к неудаче.

Заставить наивную блокировку с двойной проверкой не сработать будет сложнее... (хотя это и небезопасно без указания volatile для переменной).

13
ответ дан 2 December 2019 в 04:02
поделиться

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

| Thread 1              | Thread 2              |
|-----------------------|-----------------------|
| **start**             |                       |
| getInstance()         |                       |
| if(instance == null)  |                       |
| new ExpensiveObject() |                       |
| **context switch ->** | **start**             |
|                       | getInstance()         |
|                       | if(instance == null)  | //instance hasn't been assigned, so this check doesn't do what you want
|                       | new ExpensiveObject() |
| **start**             | **<- context switch** |
| instance = result     |                       |
| **context switch ->** | **start**             |
|                       | instance = result     |
|                       | return instance       |
| **start**             | **<- context switch** |
| return instance       |                       |
5
ответ дан 2 December 2019 в 04:02
поделиться

Поскольку это Java, вы можете использовать библиотеку thread-weaver для введения пауз или прерываний в ваш код и управления несколькими потоками выполнения. Таким образом вы можете получить медленный конструктор ExpensiveObject без необходимости изменять код конструктора, как (правильно) предлагали другие.

3
ответ дан 2 December 2019 в 04:02
поделиться
Другие вопросы по тегам:

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