【问题标题】:How to make the main thread finish last如何使主线程最后完成
【发布时间】:2021-08-05 08:57:22
【问题描述】:

我有一个在 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 list\n%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("%d\t%d\t%s\t%s\t%s\t%s\t%s\n", ID, articleID, name, brand, price, colors, url);
    }
}

【问题讨论】:

  • '结果......':不,不是。在所有非守护线程都退出之前,JVM 不会退出。您可能有一个空文件,但不是出于这个原因。这是因为您在计算完成之前打印结果。在计算线程中打印它,而不是在main()

标签: java multithreading executorservice


【解决方案1】:

让主线程等待工作线程完成的一种方法是让HTMLParser 返回其ExecutorService,以便Main.main 可以在其上调用awaitTermination(...)

或者...如果您不想将服务公开给Main 类,您可以在HTMLParser 类API 中添加“等待完成”方法。


注意:每次调用parsePage 创建然后拆除它自己的线程池执行器服务可能是个坏主意。您可能应该为每个HTMLParser 实例创建一个线程池,并在每个parsePage 调用中重用它。

此外,这样做可以更轻松地解决main 的问题。

【讨论】:

    【解决方案2】:

    有几种方法可以克服这个问题。使用可观察对象,或阻塞主线程或使用接口而不阻塞主线程。对我来说,界面将是一个不错的选择。如果你熟悉java接口,你可以实现一个接口来打印最近解析的产品。这是一步一步的方法:

    接口类:

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

    MainImpl 类(不是 Main 类本身):

    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);
                    }
                });
        }
    }
    

    未测试,但接口逻辑必须工作。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-03-25
      • 1970-01-01
      • 2019-07-09
      • 1970-01-01
      • 1970-01-01
      • 2012-03-25
      相关资源
      最近更新 更多