目录
前言
前文链接:生命游戏串行代码实现(Java)
本文将对之前的串行生命游戏进行改造,实现并行化,并通过输出运行时间来检验其之间的
效率。
在此采用的方法是程序读取给定的测试样例作为初始棋盘,随后根据样例要求进化特定代
数,输出结果文件,并和测试用例的预期输出文件进行比对,输出是否一致以及运行时间。
一、串行实现读取测试样例与输出结果
1.代码展示
import java.io.*;
import java.util.Scanner;
public class gameOfLife_1 {
private int[][] board; // 当前棋盘
private int[][] nextBoard; // 下一代棋盘
private int size; // 棋盘大小
public gameOfLife_1(int size) {
this.size = size;
board = new int[size][size];
nextBoard = new int[size][size];
}
// 从文件读取初始棋盘
private void initializeBoardFromFile(String filename) {
try (BufferedReader br = new BufferedReader(new FileReader(filename))) {
StringBuilder boardBuilder = new StringBuilder();
// 读取每一行,构建棋盘内容
String line;
while ((line = br.readLine()) != null) {
boardBuilder.append(line);
}
String boardString = boardBuilder.toString();
// 根据读取到的字符数动态确定棋盘大小(16个字符则构建4*4棋盘,1000000个字符则构建1000*1000棋盘)
size = (int) Math.sqrt(boardString.length());
board = new int[size][size];
nextBoard = new int[size][size];
// 填充棋盘
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
board[i][j] = boardString.charAt(i * size + j) - '0'; // 将字符转换为数字
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 计算下一代棋盘
private void updateBoard() {
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
int liveNeighbors = countLiveNeighbors(i, j);
if (board[i][j] == 1) { // 当前细胞为生
nextBoard[i][j] = (liveNeighbors == 2 || liveNeighbors == 3) ? 1 : 0;
} else { // 当前细胞为死
nextBoard[i][j] = (liveNeighbors == 3) ? 1 : 0;
}
}
}
// 更新棋盘状态
for (int i = 0; i < size; i++) {
System.arraycopy(nextBoard[i], 0, board[i], 0, size);
}
}
// 计算某个细胞周围的活细胞数量
private int countLiveNeighbors(int x, int y) {
int count = 0;
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
int nx = x + i;
int ny = y + j;
if(nx >= 0 && nx < size && ny >= 0 && ny < size && (i != 0 || j != 0)){
if(board[nx][ny] == 1){
count++;
}
}
}
}
return count;
}
// 启动生命游戏
public void run(int generations) {
for (int generation = 0; generation < generations; generation++) {
updateBoard();
}
}
// 将当前棋盘输出到文件
private void outputBoardToFile(String filename) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter(filename))) {
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
bw.write(board[i][j] + "");
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 比较输出结果与预期结果
private boolean compareOutputWithExpected(String expectedFilename) {
try (BufferedReader expectedReader = new BufferedReader(new FileReader(expectedFilename));
BufferedReader outputReader = new BufferedReader(new FileReader("test_sample\\output.txt"))) {
String expectedLine = expectedReader.readLine();
String outputLine = outputReader.readLine();
return expectedLine.equals(outputLine); // 直接比较
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入测试文件的序号 (1-6): ");
int testFileIndex = scanner.nextInt();
System.out.print("请输入进化代数: ");
int generations = scanner.nextInt();
gameOfLife_1 game = new gameOfLife_1(0); // 初始化大小为0
// 根据用户输入的序号生成文件名test_sample/1.end.txt
String initFilePath = String.format("test_sample\\%d.init.txt", testFileIndex);
String expectedFilePath = String.format("test_sample\\%d.end.txt", testFileIndex);
game.initializeBoardFromFile(initFilePath);
long multiThreadStartTime = System.nanoTime();
game.run(generations);
long multiThreadEndTime = System.nanoTime();
double multiThreadDuration = (multiThreadEndTime - multiThreadStartTime) / 1_000_000.0; // 转换为毫秒
System.out.println("多线程执行时间: " + multiThreadDuration + " ms");
game.outputBoardToFile("test_sample\\output.txt");
boolean isEqual = game.compareOutputWithExpected(expectedFilePath);
if (isEqual) {
System.out.println("输出结果与测试用例一致。");
} else {
System.out.println("输出结果与测试用例不一致。");
}
scanner.close();
}
}
2.详细解释
以上代码主要变化有:1.棋盘的初始化不再是赋予随机值,而是读取特定的文件,根据文件里
的数值来赋予生死状态(1为生,0为死);2.将迭代结束后的棋盘输出到文件;3.输出的结果文件
与预期结果文件进行比对,得到比对结果和运行时间。
1)读取文件
- BufferRead:通过BufferReader读取文件内容,效率较高,比较适合逐行处理。
- StringBuilder:使用StringBuilder拼接读取到的每行内容,构建棋盘。
- 动态棋盘大小:使用Math.sqrt()计算字符数的平方根,确定棋盘大小(棋盘限定为N*N的正方形,使用动态棋盘大小是因为测试样例文件中有16个字符样例,也有1000*1000的样例)
- 循环填充棋盘:使用双重循环遍历,依次从boardString中读取字符并转为正数,填入数组。
- 字符转换:boardString.charAt(i * size + j ) - '0'将字符转为对应的整数数值(0/1)
2)将最终棋盘输出到文件
- BufferWriter:使用BufferWriter写入文件。
- 写入文件:同样使用双重循环遍历棋盘写入数值。
- 由于测试样例与输出样例的格式,无需换行。
3)输出文件与预期结果对比
- BufferReader:同样是使用BufferReader读取输出文件和预期结果文件。
- 逐行对比:使用while循环和equals()方法来比较两文件的内容。
- 末尾处理:在完成逐行比对后,检查是否还有剩余行。如果一个文件还有剩余行而另一个已经读完,则也认为不一致。
3.输出结果


二、并行化
1.代码解释
实现生命游戏并行化本文使用了线程池,因此将棋盘的更新封装成了一个类,用于线程池提交任务。此类代码如下:
public class BoardUpdate implements Runnable{
private int startRow;
private int endRow;
private int[][] board;
private int[][] nextBoard;
private int size;
public BoardUpdate(int startRow, int endRow, int[][] board, int[][] nextBoard, int size) {
this.startRow = startRow;
this.endRow = endRow;
this.board = board;
this.nextBoard = nextBoard;
this.size = size;
}
//生命游戏规则
@Override
public void run(){
for (int i = startRow; i < endRow; i++) {
for (int j = 0; j < size; j++) {
int liveNeighbors = countLiveNeighbors(i, j);
if (board[i][j] == 1) {
nextBoard[i][j] = (liveNeighbors == 2 || liveNeighbors == 3) ? 1 : 0;
} else {
nextBoard[i][j] = (liveNeighbors == 3) ? 1 : 0;
}
}
}
}
//计算邻居
private int countLiveNeighbors(int x, int y) {
int count = 0;
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
int nx = x + i;
int ny = y + j;
if(nx >= 0 && nx < size && ny >= 0 && ny < size && (i != 0 || j != 0)){
if(board[nx][ny] == 1){
count++;
}
}
}
}
return count;
}
}
主类代码与串行实现的主类代码差别不大,主要差别体现在updateBoard()方法上,并行化在
此处使用了线程池来实现多线程。numThreads为定义的线程数量。采用了数据分解方法,根据线
程数量,给每个线程分配处理size / numThreads 行。在确保所有提交给线程池的任务完成后,主
线程才继续执行(否则可能会出现主程序在工作线程完成之前就执行的情况)。该方法代码如下:
private void updateBoard() {
int numThreads = 8;
//创建线程池
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
//每个线程处理的行数
int rowsPerThread = size / numThreads;
//循环创建线程
for (int i = 0; i < numThreads; i++) {
int startRow = i * rowsPerThread;
int endRow = (i == numThreads - 1) ? size : (i + 1) * rowsPerThread;
executor.submit(new BoardUpdate(startRow, endRow, board, nextBoard, size));
}
// 等待所有线程完成任务
executor.shutdown();
try {
executor.awaitTermination(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 更新棋盘状态
for (int i = 0; i < size; i++) {
System.arraycopy(nextBoard[i], 0, board[i], 0, size);
}
}
如有需要,以下是并行实现生命游戏的主类完整代码:
import java.io.*;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class gameOfLife_2 {
private int[][] board; // 当前棋盘
private int[][] nextBoard; // 下一代棋盘
private int size; // 棋盘大小
public gameOfLife_2(int size) {
this.size = size;
board = new int[size][size];
nextBoard = new int[size][size];
}
// 从文件读取初始棋盘
private void initializeBoardFromFile(String filename) {
try (BufferedReader br = new BufferedReader(new FileReader(filename))) {
StringBuilder boardBuilder = new StringBuilder();
// 读取每一行,构建棋盘内容
String line;
while ((line = br.readLine()) != null) {
boardBuilder.append(line);
}
String boardString = boardBuilder.toString();
// 根据读取到的字符数动态确定棋盘大小(16个字符则构建4*4棋盘,1000000个字符则构建1000*1000棋盘)
size = (int) Math.sqrt(boardString.length());
board = new int[size][size];
nextBoard = new int[size][size];
// 填充棋盘
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
board[i][j] = boardString.charAt(i * size + j) - '0'; // 将字符转换为数字
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 计算下一代棋盘
private void updateBoard() {
int numThreads = 8;
//创建线程池
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
//每个线程处理的行数
int rowsPerThread = size / numThreads;
//循环创建线程
for (int i = 0; i < numThreads; i++) {
int startRow = i * rowsPerThread;
int endRow = (i == numThreads - 1) ? size : (i + 1) * rowsPerThread;
executor.submit(new BoardUpdate(startRow, endRow, board, nextBoard, size));
}
// 等待所有线程完成任务
executor.shutdown();
try {
executor.awaitTermination(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 更新棋盘状态
for (int i = 0; i < size; i++) {
System.arraycopy(nextBoard[i], 0, board[i], 0, size);
}
}
// 启动生命游戏
public void run(int generations) {
for (int generation = 0; generation < generations; generation++) {
updateBoard();
}
}
// 将当前棋盘输出到文件
private void outputBoardToFile(String filename) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter(filename))) {
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
bw.write(board[i][j] + "");
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 比较输出结果与预期结果
private boolean compareOutputWithExpected(String expectedFilename) {
try (BufferedReader expectedReader = new BufferedReader(new FileReader(expectedFilename));
BufferedReader outputReader = new BufferedReader(new FileReader("test_sample\\output.txt"))) {
String expectedLine = expectedReader.readLine();
String outputLine = outputReader.readLine();
return expectedLine.equals(outputLine); // 直接比较
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入测试文件的序号 (1-6): ");
int testFileIndex = scanner.nextInt();
System.out.print("请输入进化代数: ");
int generations = scanner.nextInt();
gameOfLife_2 game = new gameOfLife_2(0); // 初始化大小为0
// 根据用户输入的序号生成文件名test_sample/1.end.txt
String initFilePath = String.format("test_sample\\%d.init.txt", testFileIndex);
String expectedFilePath = String.format("test_sample\\%d.end.txt", testFileIndex);
game.initializeBoardFromFile(initFilePath);
long multiThreadStartTime = System.nanoTime();
game.run(generations);
long multiThreadEndTime = System.nanoTime();
double multiThreadDuration = (multiThreadEndTime - multiThreadStartTime) / 1_000_000.0; // 转换为毫秒
System.out.println("多线程执行时间: " + multiThreadDuration + " ms");
game.outputBoardToFile("test_sample\\output.txt");
boolean isEqual = game.compareOutputWithExpected(expectedFilePath);
if (isEqual) {
System.out.println("输出结果与测试用例一致。");
} else {
System.out.println("输出结果与测试用例不一致。");
}
scanner.close();
}
}
2.输出结果


*此处对比单线程生命游戏所需时间更长,因为此时算入了创建线程所需的时间(待优化


可以观察到当数据量上来之后,多线程的优势便体现出来了,运行效率上升明显。
更多测试结果有兴趣的可以自行测试。
总结
以上就是本文的全部内容,主要将之前的串行生命游戏代码进行改造,在验证了生命游戏规
则算法的正确性的同时,也体现了多线程编程的优势。
通过使用线程池,我们有效地分配了任务,确保每个线程处理特定行的细胞状态,最大化利
用计算资源。同时,增加了对结果的一致性检查,确保程序输出的正确性和可靠性。
这种并行化的改造不仅提高了程序的性能,还为以后的扩展和优化提供了基础。希望本文能
为大家提供有关并行计算和生命游戏实现的启示,鼓励更多人探索多线程编程的潜力。

被折叠的 条评论
为什么被折叠?



