#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);
}
});
}
}
Не проверено, но логика интерфейса должна работать.