对于搬箱子,走迷宫这里问题,都包含一个初始位置,一个目标位置,一个目标集(起点与目标之间的有效移动),一个规则集(给定位置的合法移动,计算某位置的可能结果)这种问题称作一个谜题,首先看一下单线程中怎么解决这一来问题。
对这种问题进行抽象,其中:P 和M 代表位置和移动类。:
public interface Puzzle<P,M> {
//初始位置
P initialPosition();
//是否为终点
boolean isGoal(P position);
//目前所在位置下一步可以移动的集合
Set<M> legalMoves(P position);
//一次移动过程
P move(P position,M move);
}
每次移动的节点:
class Node<P, M> {
final P pos;
final M move;
final Node<P, M> prev;
public Node(P pos, M move, Node<P, M> prev) {
super();
this.pos = pos;
this.move = move;
this.prev = prev;
}
List<M> asMoveList(){
List<M> solution=new LinkedList<>();
for(Node<P, M> node=this;node.move!=null;node=node.prev) {
solution.add(0,node.move);
}
return solution;
}
}
顺序化的解决问题:
public class SequentialPuzzleSolver<P,M> {
private final Puzzle<P, M> puzzle;
private final Set<P> seen=new HashSet<>();
public SequentialPuzzleSolver(Puzzle<P, M> puzzle) {
this.puzzle=puzzle;
}
public List<M> search(Node<P, M> node){
if (!seen.contains(node.pos)) {
seen.add(node.pos);
if (puzzle.isGoal(node.pos)) {
return node.asMoveList();
}
for(M move:puzzle.legalMoves(node.pos)) {
P pos=puzzle.move(node.pos, move);
Node<P, M> child=new Node<P, M>(pos, move, node);
List<M> result=search(child);
if (result!=null) {
return result;
}
}
}
return null;
}
}
SequentialPuzzleSolver是一个谜题框架的顺序话解决器,它在谜题空间中执行一个深度优先搜索,找到一个结果后终止,但并不一定是最优解。
因为每次计算和移动之间很少有资源竞争。因此有多个处理器可用时可以减少寻找结果所花费的时间。可以改写前一个解决过程,并加强一些并发性。串行顺序版本的程序执行就是深度优先搜索,搜索能力受到栈空间大小的限制,在java中会抛出(StackOverflowError)。
并发版本执行的是广度优先搜索,因此不会收到栈大小的影响,但仍然可能派出OOM异常(待搜索的结果集,已经访问过的记录超出内存可用空间)。
为了发现程序找到一个解法后可以停止搜索行为,需要一种方式来检查是否有哪个线程已经找出结果。如果我们需要获取程序第一个被计算出的结果,程序只有在没有发现任何方法时才去更新方案。这些条件描述了一种闭锁(latch)的行为,是一种可以携带结果的闭锁。
每个任务都会向闭锁请求方案,如果发现方案已经被发现,就停止。在找到一个方案之前,主线程需要等待。ValueLatch中的getValue会一直阻塞,直到有线程设置了value.
ValueLatch:
public class ValueLatch<T> {
private T value=null;
private final CountDownLatch done=new CountDownLatch(1);
public boolean isSet() {
return (done.getCount()==0);
}
public synchronized void setValue(T newValue) {
if (!isSet()) {
value=newValue;
done.countDown();
}
}
public T getValue() throws InterruptedException {
done.await();
synchronized (this) {
return value;
}
}
}
并行的解决问题:
public class ConcurrentPuzzleSolver<P,M> {
private final Puzzle<P, M> puzzle;
private final ExecutorService exec;
private final ConcurrentHashMap<P, Boolean> seen;
final ValueLatch<Node<P, M>> solution
=new ValueLatch<>();
public ConcurrentPuzzleSolver(Puzzle<P, M> puzzle, ExecutorService exec, ConcurrentHashMap<P, Boolean> seen) {
super();
this.puzzle = puzzle;
this.exec = exec;
this.seen = seen;
}
public List<M> solve() throws InterruptedException{
try {
P p=puzzle.initialPosition();
exec.execute(newTask(p, null, null));
Node<P, M> solnNode=solution.getValue();
return solnNode==null?null:solnNode.asMoveList();
}finally {
exec.shutdown();
}
}
protected Runnable newTask(P pos,M move,Node<P, M> prev) {
return new SolverTask(pos, move, prev);
}
class SolverTask extends Node<P, M> implements Runnable{
public SolverTask(P pos, M move, Node<P, M> prev) {
super(pos, move, prev);
}
@Override
public void run() {
if (solution.isSet()||seen.putIfAbsent(pos, true)!=null) {
return ;
}
if (puzzle.isGoal(pos)) {
solution.setValue(this);
}else {
for(M m:puzzle.legalMoves(pos)) {
exec.execute(newTask(pos, m, prev));
}
}
}
}
}
但是以上代码存在一个问题,假如程序搜索完了所有的位置,但是还没有找到具体的方案。程序会一直阻塞。为了解决这个问题,可以在获取结果时设置超时时间,或者增加一个计数器,当所有节点搜索完成时将结果设置为null。
可以判断出任务不存在的解决问题:
public class PuzzleSolver<P,M> extends ConcurrentPuzzleSolver<P, M> {
private final AtomicInteger taskCount=new AtomicInteger(0);
public PuzzleSolver(Puzzle<P, M> puzzle, ExecutorService exec, ConcurrentHashMap<P, Boolean> seen) {
super(puzzle, exec, seen);
}
class CountingSolverTask extends ConcurrentPuzzleSolver<P,M>.SolverTask{
public CountingSolverTask(ConcurrentPuzzleSolver<P, M> concurrentPuzzleSolver, P pos, M move, Node<P,M> prev) {
super(pos, move, prev);
//创建一个任务时计数器加1
taskCount.incrementAndGet();
}
public void run() {
try {
super.run();
} finally {
//当计数器再次回到0时,代表已经搜索完成
if (taskCount.decrementAndGet()==0) {
solution.setValue(null);
}
}
}
}
}
-------------------------------分割线-----------------------------------
参考资料:<<java并发变成实践>>