You can use Blocking Queues as buffers. They handle everything as far as getting threads to wait for other threads when the queues are empty.
Basically you'll have two classes, one for each thread. So, you'll have something like this.
class PageToRetriveQueue implements Runnable{
PageBuffer partner;
BlockingQeueue queue = new LinkedBlockingQueue<Page>();
public void run(){
while(true){
Page p = partner.queue.take();
for(Link l : p){
queue.offer(l);
}
}
}
}
class PageBuffer implements Runnable{
PageToRetriveQueue partner;
BlockingQeueue queue = new LinkedBlockingQueue<Link>();
public void run(){
while(true){
Link l = partner.queue.take();
Page p = downloadPage(l);
queue.offer(p);
}
}
}
You'll have to implement the Page, Link, and downloadPage functions. When you start, you'll have to seed one of the queues in order to get started, probably the link queue. It's stylistically bad form to call partner.queue.take() directly, rather you'd have a function that would abstract that. I'm trying to make the code concise and easy to understand here.
Hope that helps!