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

#java #multithreading #executorservice

Вопрос:

У меня есть парсер, который работает с помощью Executor.newCachedThreadPool (), и столкнулся с тем, что основной поток, в который записывается запись в файл JSON, выполняется раньше дочерних. В результате мы имеем пустой файл… Я довольно плохо знаю тему многопоточности и не могу понять, в чем дело. Я попытался использовать метод Join () в основном потоке, но в конце концов программа просто зависает при приближении к этой части

Main.java

 import model.Product;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class Main {

    public static void main(String[] args) throws InterruptedException {

        String rootUrl = "example.com";
        System.out.println("Started parsing: "   rootUrl);
        long m = System.currentTimeMillis();

        HtmlParser htmlParser = new HtmlParser();
        List<Product> productList = new CopyOnWriteArrayList<>();
        htmlParser.parse(rootUrl, productList);

        Printer.printToJson(productList);

        System.out.println("Finish: completed in "   ((double) System.currentTimeMillis() - m) / 1000   " seconds");
    }
}
 

HtmlParser.java

 import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import ua.bala.model.Product;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class HtmlParser {

    private static AtomicInteger httpRequestsCounter = new AtomicInteger(0);

    public static AtomicInteger getHttpRequestsCounter() {
        return httpRequestsCounter;
    }

    public void parse(String url, List<Product> productList) {
        try {
            Document page = getPage(url);
            parsePage(page, productList);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static Document getPage(String url) throws IOException {
        Document document = Jsoup.connect(url).get();
        httpRequestsCounter.getAndIncrement();
        return document;
    }

    private void parsePage(Document page, List<Product> productList) {
        Elements productElements = page.select("a.dgBQdu");

        ExecutorService service = Executors.newCachedThreadPool();
        for (Element element: productElements){
            service.execute(() -> {

                Long articleID = Long.parseLong(element.attr("id"));
                String name = "NAME";
                String brand = "BRAND";
                BigDecimal price = new BigDecimal(BigInteger.ZERO);
                Set<String> colors = new HashSet<>();
                String url = "https://www.aboutyou.de"   element.attr("href");
                Document innerPage;

                try {
                    innerPage = getPage(url);
                    Element innerElement = innerPage.selectFirst("[data-test-id='BuyBox']");
                    name = innerElement.selectFirst("div.dZjUXd").text();
                    brand = innerElement.selectFirst("[data-test-id='BrandLogo']").attr("alt");
                    colors = new HashSet<>(innerElement.select("span.jlvxcb-1").eachText());
                    String priceStr = innerElement.selectFirst("div.dWWxvw > span").text().replace("ab ","").replace(" EUR","").replace(",", ".");
                    price = new BigDecimal(priceStr);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                Product product = new Product(articleID, name, brand, colors, price, url);
                addProduct(product, productList);
            });
        }
        service.shutdown();
    }

    private synchronized void addProduct(Product product, List<Product> productList){
        System.out.println("Product "   product.getID()   " parsed");
        System.out.print(product);
        productList.add(product);
        System.out.printf("Product %d added to listn%n", product.getID());
    }
}
 

Printer.java

 import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import model.Product;

import java.io.*;
import java.util.Comparator;
import java.util.List;

public class Printer {

    private static final String path = "";
    private static final String fileName = "productsOutput";

    public static void printToJson(List<Product> products){

        products.sort(Comparator.comparing(Product::getID));

        System.out.println("Product list start printing to JSON");
        try (final Writer writer = new FileWriter(path   fileName   ".json")) {
            Gson gson = new GsonBuilder().create();
            gson.toJson(products, writer);
            System.out.println("Product list printed to JSON");
            System.out.printf("Amount of triggered HTTP requests: %s%nAmount of extracted products: %s%n",
                                 HtmlParser.getHttpRequestsCounter(), products.size());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
 

Printer.java

 package model;

import lombok.*;

import java.math.BigDecimal;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

@NoArgsConstructor
@Getter
@Setter
public class Product {

    private static AtomicLong productsCounter = new AtomicLong(1);

    private Long ID;
    private Long articleID;
    private String name;
    private String brand;
    private BigDecimal price;
    private Set<String> colors;
    private String url;

    {
        ID = productsCounter.getAndIncrement();
    }

    public Product(Long articleID, String name, String brand, Set<String> colors, BigDecimal price, String url) {
        this.articleID = articleID;
        this.name = name;
        this.brand = brand;
        this.price = price;
        this.colors = colors;
        this.url = url;
    }

    public static AtomicLong getProductsCounter() {
        return productsCounter;
    }

    @Override
    public String toString() {
        return String.format("%dt%dt%st%st%st%st%sn", ID, articleID, name, brand, price, colors, url);
    }
}
 

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

1. «В результате…»: нет, это не так. JVM не завершит работу до тех пор, пока не будут завершены все потоки, не являющиеся демонами. У вас может быть пустой файл, но не по этой причине. Это связано с тем, что вы печатаете результат до завершения вычисления. Распечатайте его в потоке вычислений, а не в main() .

Ответ №1:

Одним из способов заставить основной поток дождаться завершения рабочих потоков было бы HTMLParser вернуть его ExecutorService , чтобы Main.main он мог вызвать awaitTermination(...) его.

Или… если вы не хотите предоставлять услугу Main классу, вы можете добавить метод «подождите, пока это не будет сделано» HTMLParser в API классов.


Примечание: вероятно, это плохая идея для каждого вызова parsePage , чтобы создать, а затем разрушить собственную службу исполнителя пула потоков. Вероятно, вам следует создать один пул потоков для каждого HTMLParser экземпляра и повторно использовать его при каждом parsePage вызове.

Кроме того, это облегчит решение вашей проблемы с main помощью.

Ответ №2:

Есть несколько способов преодолеть это. Использование наблюдаемых объектов, или блокировка основного потока, или использование интерфейса без блокировки основного потока. Для меня интерфейс был бы хорошим выбором. Если вы знакомы с интерфейсом java, вы можете реализовать интерфейс для печати недавно проанализированных продуктов. Вот как шаг за шагом:

Класс интерфейса:

 public interface ProductsListener {
    void onProductsReady(List<Product> products);
}
 

Класс MainImpl (НЕ сам основной класс):

 public class MainImpl implements ProductListener {
    // When product list loading is done this func will be called
    void onProductsRead(List<Product> products) {
        Printer.printToJson(productList);
    }
}
 

В основном классе:

 public class Main {
    public static void main(String[] args) throws InterruptedException {
        MainImpl listener = new MainImpl();
        htmlParser.setProductListener(listener);
        // Rest of the code...
    }
}
 

В классе HTMLParser:

 public class HtmlParser {
    private MainImpl productListener;
    //...

    public void setProductListener(MainImpl listener) {
        // Alternatively you can do it in a constructor
        productListener = listener;
    }
    //...

    private void parsePage(Document page, List<Product> productList) {
        Elements productElements = page.select("a.dgBQdu");
        int parseCount = 0;

        ExecutorService service = Executors.newCachedThreadPool();
        for (Element element: productElements){
            service.execute(() -> {

                Long articleID = Long.parseLong(element.attr("id"));
                String name = "NAME";
                String brand = "BRAND";
                BigDecimal price = new BigDecimal(BigInteger.ZERO);
                Set<String> colors = new HashSet<>();
                String url = "https://www.aboutyou.de"   element.attr("href");
                Document innerPage;

                try {
                    innerPage = getPage(url);
                    Element innerElement = innerPage.selectFirst("[data-test-id='BuyBox']");
                    name = innerElement.selectFirst("div.dZjUXd").text();
                    brand = innerElement.selectFirst("[data-test-id='BrandLogo']").attr("alt");
                    colors = new HashSet<>(innerElement.select("span.jlvxcb-1").eachText());
                    String priceStr = innerElement.selectFirst("div.dWWxvw > span").text().replace("ab ","").replace(" EUR","").replace(",", ".");
                    price = new BigDecimal(priceStr);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                Product product = new Product(articleID, name, brand, colors, price, url);
                addProduct(product, productList);
                parseCount  ; // Count each element that has been parsed
                // Check if all elements have been parsed
                if(parseCount >= productElements.size()) {
                    // All products are done, notify the listener class
                    productListener.onProductsReady(productList);
                }
            });
    }
}
 

Не проверено, но логика интерфейса должна работать.