由于您不想像 EricF 建议的那样对其进行分块,我建议您实现自己的执行程序,以便为您分块。这仍然允许您将每个分数计算定义为它自己的Runnable(或者更确切地说,我使用自定义功能接口,但想法是一样的)。
首先,让我们先进行速度测试:
ScoreCalculatorOriginal.java(这实际上是您的代码):
public class ScoreCalculatorOriginal {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ArrayList<Callable<Object>> tasks = new ArrayList<>();
final double[] bestScore = { Double.MAX_VALUE };
for(int i = 0; i < 100000; i++) {
tasks.add(Executors.callable(() -> {
Random random = new Random();
double score = Math.pow(Math.sin(random.nextDouble()), 2) * Math.pow(Math.cos(random.nextDouble()), 2);
if (score < bestScore[0]) {
synchronized (bestScore) {
if (score < bestScore[0]) {
bestScore[0] = score;
}
}
}
}));
}
long start = System.nanoTime();
List<Future<Object>> futures = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
.invokeAll(tasks);
for(Future<Object> future : futures) {
future.get();
}
long end = System.nanoTime();
System.out.printf("Calculation took %.3f ms%n", (end - start) / 1e6);
}
}
8 个线程(我的核心数):
计算耗时 103.358 毫秒
4 个线程:
计算耗时 104.351 毫秒
1 个线程:
计算耗时 102.918 毫秒
它根本无法扩展。
这是我的看法:
ScoreCalculatorFast.java:
public class ScoreCalculatorFast {
public static void main(String[] args) throws InterruptedException {
ScoreExecutor executor = new ScoreExecutor(Runtime.getRuntime().availableProcessors());
List<ScoreExecutor.ScoreJob> jobs = new ArrayList<>();
for(int i = 0; i < 100000; i++) {
jobs.add(() -> {
Random random = new Random();
return Math.pow(Math.sin(random.nextDouble()), 2) * Math.pow(Math.cos(random.nextDouble()), 2);
});
}
long start = System.nanoTime();
executor.getBestScore(jobs);
long end = System.nanoTime();
System.out.printf("Calculation took %.3f ms%n", (end - start) / 1e6);
}
}
8 个线程:
计算耗时 19.624 毫秒
4 个线程:
计算耗时 24.275 毫秒
1 个线程:
计算耗时 41.357 毫秒
如您所见,它的速度明显更快,并且随着线程数的增加而扩展。尽管添加更多线程的回报会递减,因为某些工作不能分布在更多线程上,并且在计算的开始和结束时涉及一些同步。随着更高级的分数计算需要更长的时间,您会看到更多线程的更高收益。
现在这里是你的实现:
ScoreExecutor.java:
public class ScoreExecutor {
/**
* A job that calculates a score
*/
public static interface ScoreJob {
/**
* Calculate the score
* @return the calculated score
*/
double calculateScore();
}
// This is the threads that do all the work
final ArrayList<ScoreThread> threads;
ScoreExecutor(int numThreads) {
// Create the threads
threads = new ArrayList<>();
for(int i = 0; i < numThreads; i++) {
threads.add(new ScoreThread());
}
// Start them
for(ScoreThread thread : threads) {
thread.start();
}
}
/**
* Execute a collection of ScoreJobs and return the best score among them.
* @param jobs The jobs to execute
* @return The best score from the scores calculated by the jobs
* @throws InterruptedException
*/
public double getBestScore(Collection<ScoreJob> jobs) throws InterruptedException {
ArrayList<ScoreJob> jobList = new ArrayList<>(jobs);
// Start all threads
int chunkSize = jobList.size() / threads.size();
for(int i = 0; i < threads.size() - 1; i++) {
threads.get(i).startJobs(jobList.subList(i * chunkSize, (i+1) * chunkSize));
}
// Start the last thread
int lastIndex = threads.size() - 1;
threads.get(lastIndex).startJobs(jobList.subList(lastIndex * chunkSize, jobList.size()));
// Get the best score from each thread
LinkedList<Double> threadScores = new LinkedList<>();
for(ScoreThread thread : threads) {
threadScores.add(thread.getBestScore());
}
// Calculate the best score
double bestScore = Double.MAX_VALUE;
for(Double score : threadScores) {
if(score < bestScore) {
bestScore = score;
}
}
return bestScore;
}
/**
* Worker thread
*/
private class ScoreThread extends Thread {
// If we're currently running a score calculation
private volatile boolean run;
// The current best score
private volatile double bestScore;
// Latch for synchronisation with the executor
private CountDownLatch latch;
// The list of jobs to execute
private final LinkedList<ScoreJob> scoreJobs = new LinkedList<>();
private void startJobs(Collection<ScoreJob> jobs) {
synchronized (this) {
if(!run) {
// Start the thread
scoreJobs.addAll(jobs);
latch = new CountDownLatch(1);
run = true;
this.notifyAll();
} else {
throw new IllegalStateException("This thread is already running jobs");
}
}
}
/**
* Get the best score at the end of the calculation.
* Waits until all jobs are finished and then returns
* this thread's best score.
* @return This threads best score
* @throws InterruptedException
*/
private double getBestScore() throws InterruptedException {
// Wait for completion and return
latch.await();
return bestScore;
}
@Override
public void run() {
run = false;
try {
// External loop, run forever so we can run multiple jobs
while (true) {
// Wait for a job to be started
synchronized (this) {
while (!run) {
wait();
}
}
// This threads best score
double bestScore = Double.MAX_VALUE;
ScoreJob job; // The current job
// Get a job
while((job = scoreJobs.poll()) != null) {
// Calculate the score
double score = job.calculateScore();
// Update the best score
if(score < bestScore) {
bestScore = score;
}
}
// We're done, update the best score and release the latch
this.bestScore = bestScore;
latch.countDown();
// Set run to false so we wait for the next batch of jobs
run = false;
}
} catch(InterruptedException e) {
e.printStackTrace();
return;
}
}
}
}
这里的主要优化是每个线程都跟踪它的本地最佳分数,而不是更新全局最佳分数。然后,一旦所有线程都完成,我们让执行器从所有线程的最佳分数中选择最佳分数。