проблема для начинающих с многопоточностью java

#java #multithreading

#java #многопоточность

Вопрос:

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

 public class  Worker implements Runnable {

   private List<File> files ;

   public Worker(List<File> f){
       files = f;
   }

   public void run(){
     // read all files from list and rename them
   }
}
  

а затем в основном классе сделать что-то вроде:

 Worker w1 = new Worker(..list of 500 files...) ;
Worker w2 = new Worker(..list of the other 500 files...) ;

Thread t1 = new Thread(w1,"thread1");
Thread t2 = new Thread(w2,"thread2");

t1.start();
t2.start();
  

Запуск этого не вызывает у меня проблем с параллелизмом, поэтому мне не нужен синхронизированный код, но я не уверен, что это правильный подход …?

Или я должен создать только один экземпляр Worker() и передать весь список файлов 1000, и позаботиться о том, чтобы независимо от того, сколько потоков обращаются к объекту, thew не получит тот же файл из списка?

то есть :

 Worker w1 = new Worker(..list of 1000 files...) ;

Thread t1 = new Thread(w1,"thread1");
Thread t2 = new Thread(w1,"thread2");
t1.start();
t2.start();
  

Как мне поступить здесь?

Комментарии:

1. brings me no concurrency issues... Не между t1 и t2, а как насчет t1 и основного потока (соответственно t2)?

2. Первая мера: предположение о том, что большее количество потоков, выполняющих дисковый ввод-вывод (переименование файлов) на одном диске одновременно, быстрее, обычно неверно. Базовая файловая система предоставляет гарантии, которые могут потребовать использования блокировок (например, блокировки каталога при атомарном переименовании файла), что, в свою очередь, означает, что все / большинство операций синхронизированы. Дополнительное преимущество (немного) ускорения работы с многопоточным кодом не всегда перевешивает простоту / удобство обслуживания однопоточного кода.

3. вы правы, Ваноекель, но я нашел в этой программе хорошую возможность получить некоторые навыки многопоточности 🙂

Ответ №1:

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

 Worker w1 = new Worker(..list of 500 files...) ; // First List
Worker w2 = new Worker(..list of the other 500 files...) ;  // Second List
Thread t1 = new Thread(w1,"thread1");
Thread t2 = new Thread(w2,"thread2");

t1.start();
t2.start();
  

Здесь все просто: два разных потока с загрузкой в 500 файлов будут выполняться одновременно.

Ответ №2:

Более типичным и масштабируемым подходом является один из следующих:

  • создайте коллекцию (вероятно, массив или список) из N потоков для выполнения работы
  • используйте пул потоков, например, из Executors.newFixedThreadPool(N)

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

Подумайте, в конце концов, что произойдет, если все ваши файлы большего размера окажутся в корзине, обрабатываемой потоком2? Первый поток завершен / простаивает, а второй поток должен выполнять всю тяжелую работу.

Подход к объединению производителей и потребителей заключается в том, чтобы сбрасывать всю работу (сгенерированную производителем) в пул задач, а затем потребители (ваши рабочие потоки) откусывают небольшие кусочки (например, один файл) за раз. Этот подход приводит к тому, что оба потока остаются занятыми в течение одинаковой продолжительности.

Ответ №3:

При изучении многопоточного программирования одним из важных выводов является то, что поток — это не задача. Предоставляя потоку часть списка элементов для обработки, вы на полпути, но следующий шаг приведет вас дальше: построение задачи таким образом, чтобы ее могло выполнить любое количество потоков. Для этого вам нужно будет ознакомиться с java.util.concurrent классами. Это полезные инструменты, помогающие создавать задачи.

Приведенный ниже пример отделяет задачи от потоков. Он используется AtomicInteger для обеспечения того, чтобы каждый поток выбирал уникальную задачу, и он использует CountDownLatch , чтобы знать, когда вся работа выполнена. В примере также показана балансировка: потоки, которые выполняют задачи, которые выполняются быстрее, выполняют больше задач. Приведенный пример ни в коем случае не является единственным решением — существуют и другие способы сделать это, которые могут быть быстрее, проще, удобнее в обслуживании и т.д..

 import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class MultiRename implements Runnable {

public static void main(String[] args) {

    final int numberOfFnames = 50;

    MultiRenameParams params = new MultiRenameParams();
    params.fnameList = new ArrayList<String>();
    for (int i = 0; i < numberOfFnames; i  ) {
        params.fnameList.add("fname "   i);
    }
    params.fnameListIndex = new AtomicInteger();

    final int numberOfThreads = 3;

    params.allDone = new CountDownLatch(numberOfThreads);
    ExecutorService tp = Executors.newCachedThreadPool();
    System.out.println("Starting");
    for (int i = 0; i < numberOfThreads; i  ) {
        tp.execute(new MultiRename(params, i));
    }
    try { params.allDone.await(); } catch (Exception e) {
        e.printStackTrace();
    }
    tp.shutdownNow();
    System.out.println("Finished");
}

private final MultiRenameParams params;
private final Random random = new Random();
// Just to show there are fast and slow tasks.
// Thread with lowest delay should get most tasks done.
private final int delay;

public MultiRename(MultiRenameParams params, int delay) {
    this.params = params;
    this.delay = delay;
}

@Override
public void run() {

    final int maxIndex = params.fnameList.size();
    int i = 0;
    int count = 0;
    while ((i = params.fnameListIndex.getAndIncrement()) < maxIndex) {
        String fname = params.fnameList.get(i);
        long sleepTimeMs = random.nextInt(10)   delay;
        System.out.println(Thread.currentThread().getName()   " renaming "   fname   " for "   sleepTimeMs   " ms.");
        try { Thread.sleep(sleepTimeMs); } catch (Exception e) {
            e.printStackTrace();
            break;
        }
        count  ;
    }
    System.out.println(Thread.currentThread().getName()   " done, renamed "   count   " files.");
    params.allDone.countDown();
}

static class MultiRenameParams {

    List<String> fnameList;
    AtomicInteger fnameListIndex;
    CountDownLatch allDone;
}

}
  

Комментарии:

1. «поток — это не задача». Верно, но вы можете создать систему, которая запускает каждую задачу в своем собственном новом потоке (не спрашивайте меня, откуда я знаю!). Однако это не очень разумная идея, если стоимость выполнения задачи существенно не превышает стоимость создания и уничтожения потоков, и даже тогда это может быть неразумно.

2. @jameslarge Вот где ThreadPool появляется a. Однажды я переработал программу, чтобы использовать один пул потоков и заменить new Thread(task).start() на ThreadPool.execute(task) : это было относительно легко сделать, и увеличение производительности было заметным. Единственное, на что следует обратить внимание, это количество похожих задач, выполняемых одновременно, например. ограничьте количество задач, работающих с локальными файлами, до 8 (или любого другого подходящего) вместо того, чтобы позволять пулу потоков расти до сотен потоков, выполняющих задачи, используя один и тот же (ограниченный) ресурс.