7

У меня нет никакого способа, чтобы объяснить это одно, но я нашел это явление в чужом коде:SecurityException из кода I/O в параллельном потоке

import java.io.IOException; 
import java.io.UncheckedIOException; 
import java.nio.file.Files; 
import java.util.stream.Stream; 

import org.junit.Test; 

public class TestDidWeBreakJavaAgain 
{ 
    @Test 
    public void testIoInSerialStream() 
    { 
     doTest(false); 
    } 

    @Test 
    public void testIoInParallelStream() 
    { 
     doTest(true); 
    } 

    private void doTest(boolean parallel) 
    { 
     Stream<String> stream = Stream.of("1", "2", "3"); 
     if (parallel) 
     { 
      stream = stream.parallel(); 
     } 
     stream.forEach(name -> { 
      try 
      { 
       Files.createTempFile(name, ".dat"); 
      } 
      catch (IOException e) 
      { 
       throw new UncheckedIOException("Failed to create temp file", e); 
      } 
     }); 
    } 
} 

При запуске с менеджером безопасности включен, просто позвонив parallel() по потоку, или parallelStream() при получении потока из коллекции, похоже, гарантирует, что все попытки выполнить ввод-вывод вызовут SecurityException. (Скорее всего, вызов любого метода, который может бросить SecurityException, будет бросок.)

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

Удаление вызовов до parallel() или parallelStream() на всем протяжении кода позволяет избежать риска. Вставка AccessController.doPrivileged также исправляет его, но не кажется мне безопасным, по крайней мере, не во всех ситуациях. Есть ли другой вариант?

+2

Просьба указать трассировку стека исключения, которое вы получаете –

+2

Также добавьте код 'SecurityManager', чтобы мы могли точно воспроизвести вашу проблему. –

+1

Возможно, это связано с параллельными потоками, использующими общий пул соединений fork. Как это работает [если вы предоставляете свой собственный пул] (http://stackoverflow.com/a/22269778/829571)? – assylias

ответ

6

Параллельное выполнение потока будет использовать фреймворк Fork/Join, более конкретно он будет использовать общий пул Fork/Join. Это детализация реализации, но, как наблюдалось в этом случае, такие детали могут просачиваться неожиданными способами.

Обратите внимание, что при выполнении задачи асинхронно выполняется такое же поведение, используя CompletableFuture.

Когда присутствует менеджер безопасности, фабрика нитей в общем пуле Fork/Join устанавливается на фабрику, которая создает безвредные потоки. Такой поток безвредный не имеет предоставленных ему разрешений, не является членом какой-либо определенной группы потоков, а после завершения выполнения задачи Fork/Join верхнего уровня все локаторы потоков (если они созданы) очищаются. Такое поведение гарантирует, что задачи Fork/Join изолированы друг от друга при совместном использовании общего пула.

Вот почему в например SecurityException брошено, вероятно:

java.lang.SecurityException: Невозможно создать временный файл или каталог

Есть два возможных обходные работы. В зависимости от причин, используемых менеджером безопасности, каждая работа может увеличить риск небезопасности.

Первый, более общий, рабочий процесс - зарегистрировать фабрику Fork/Join thread через системное свойство, чтобы сообщить структуре Fork/Join, что завод по умолчанию должен быть для общего пула. Например здесь очень простая нить завод:

public class MyForkJoinWorkerThreadFactory 
     implements ForkJoinPool.ForkJoinWorkerThreadFactory { 
    public final ForkJoinWorkerThread newThread(ForkJoinPool pool) { 
     return new ForkJoinWorkerThread(pool) {}; 
    } 
} 

который может быть зарегистрирован следующим свойством системы:

-Djava.util.concurrent.ForkJoinPool.common.threadFactory = MyForkJoinWorkerThreadFactory

В настоящее время поведение MyForkJoinWorkerThreadFactory эквивалентно поведению ForkJoinPool.defaultForkJoinWorkerThreadFactory.

Вторая, более конкретная, работа над созданием нового вилка/объединения пула. В этом случае ForkJoinPool.defaultForkJoinWorkerThreadFactory будет использоваться для конструкторов, не принимающих аргумент ForkJoinWorkerThreadFactory. Любое выполнение параллельного потока должно выполняться из задачи, выполняемой из этого пула. Обратите внимание, что это подробная информация о реализации и может работать или не работать в будущих выпусках.

+0

Хороший ответ. Также имейте в виду, что подающая нить может использоваться как рабочая нить в структуре F/J, которая может иметь влияние здесь: http://coopsoft.com/ar/Calamity2Article.html#submit – edharned

+0

Интересно, что даже создание нового ForkJoinPool без настройки ничего создает пул, который дает мне полные разрешения. Это только экземпляр, который исходит от commonPool(), который был «отравлен» их изготовителем. Похоже, что Stream API не дает мне возможности выбирать, какой пул используется в любом случае, поэтому я думаю, нам просто нужно будет не использовать его и использовать наши существующие утилиты параллельного выполнения. – Trejkaz

+0

Trejkaz, вы правы, я пропустил это при просмотре кода. Ответ обновлен, чтобы исправить ошибку. –

2

Ваше беспокойство по поводу AccessController.doPrivileged не нужно. Это не уменьшает безопасность если сделано правильно. Версии, принимающие один аргумент действия будут выполнять действия в вашем контексте, игнорируя звонящие, но есть перегруженные методы, имеющие дополнительный аргумент, ранее записанная контекст:

private void doTest(boolean parallel) 
{ 
    Consumer<String> createFile=name -> { 
     try { 
      Files.createTempFile(name, ".dat"); 
     } 
     catch (IOException e) { 
      throw new UncheckedIOException("Failed to create temp file", e); 
     } 
    }, actualAction; 
    Stream<String> stream = Stream.of("1", "2", "3"); 

    if(parallel) 
    { 
     stream = stream.parallel(); 
     AccessControlContext ctx=AccessController.getContext(); 
     actualAction=name -> AccessController.doPrivileged(
      (PrivilegedAction<?>)()->{ createFile.accept(name); return null; }, ctx); 
    } 
    else actualAction = createFile; 

    stream.forEach(actualAction); 
} 

Первая важная строка является AccessControlContext ctx=AccessController.getContext(); заявления он записывает текущий контекст безопасности, который включает в себя код и текущих абонентов. (Помните, что эффективными разрешениями являются перекрестки наборов всех абонентов). Предоставляя полученный объект контекста ctx методу doPrivileged в пределах Consumer, вы восстанавливаете контекст, другими словами, PrivilegedAction будет иметь те же разрешения, что и в вашем однопоточном сценарии.

Смежные вопросы