Home | Contact Us | FAQ | Search & Site Map | Link to Us
Sign In | Join | Other 45 Sites in Network
HomeAnnouncementsWhite Papers
Discussion GroupsFirst AidDatabasesJavaBeansGUIJava 3DVirtual MachineCORBASecurityToolsGeneral
Java DirectoryOpen Source ProjectsSample Book ChaptersUser GroupsWeb Resources
Related Topics
Databases.NETMore Topics ...

Java Forum / General / April 2007

Tip: Looking for answers? Try searching our database.

Don't Understand wait() and notify()

Thread view: 
Jason Cavett - 28 Mar 2007 23:30 GMT
I will be the first to admit I don't *really* understand threads.
Have a general idea, but, as I haven't had to use them too often, I
never really got a chance to learn them.  Well...I'm working with them
now and that leads me to my question.

I am attempting to create an application that queues up files (given
by the user via the GUI) that will be processed.  Whenever a file is
added to the queue, the processor takes the file off the queue and
processes it.  The user should be able to continue working in the GUI
normally (as they would expect).

So far, I have three components to this aspect of my application.
They are:

ProjectModel - This is a user's project.  It holds a list of the files
they're working on.  This also provides the model that the GUI (View)
is created from.  Anyway, what's important is that the ProjectModel
holds a "FileProcessor" object.

FileProcessor - The FileProcessor is simply a class that has a Vector
of Files.  It provides (synchronized) methods to enqueue and dequeue
files.  The FileProcessor has a FileWorkerThread object.  The
FileWorkerThread is started in the FileProcessor's constructor.

FileWorkerThread - The FileWorkerThread object has a run() method
(implements Runnable) and does the work of processing the file.
FileWorkerThread also has a reference to the FileProcessor object (the
same exact object as above - it's passed in on the FileWorkerThread's
constructor).

The issue I am having, is I don't understand how wait and notify apply
in this case.  I was initially attempting to use the FileProcessor
object as a lock.  When the FileWorkerThread started up, if nothing
was in the queue of the FileProcessor, the FileWorkerThread was
supposed to wait until there was something in the queue.
Unfortunately, when I called wait() on the FileProcessor object (the
one that I was synchronizing over) the GUI would hang.

What I am attempting to have is something like this:
Open up a Project -> Queue a file in FileProcessor -> Notify the
FileWorkerThread that there are items waiting for it in the queue ->
FWT does the work -> When no more items are in the queue, FWT waits
until it receives another notify

Here is the code for the FileProcessor and the FileWorkerThread:

---
public class FileProcessor {

   private Vector<File> files;

   private FileWorkerThread worker;

   public FileProcessor() {
       files = new Vector<File>();

       // worker thread setup
       worker = new FileWorkerThread(this);
       Thread workerThread = new Thread(worker, "Worker Thread");
       workerThread.run();
   }

   public synchronized void enqueueFile(File file) {
       files.add(file);
   }

   public synchronized File dequeueFile() {
       File file = null;

       if (files.size() != 0) {
           file = files.firstElement();
           files.remove(file);
       }

       return file;
   }

   public synchronized void removeFile(File file) {
       files.remove(file);
   }

   public synchronized void removeFile(int position) {
       files.remove(position);
   }

   public synchronized void notifyWorker() {
       this.notify();
   }

   public synchronized void waiting() {
       try {
           this.wait();
           this.notify();
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }
}
---
public class FileWorkerThread implements Runnable {

   private FileProcessor processor;

   public FileWorkerThread(FileProcessor processor) {
       this.processor = processor;
   }

   /**
    * @see java.lang.Runnable#run()
    */
   public void run() {
       // work forever
       while (true) {
           // check for work
           File file = processor.dequeueFile();

           if (file == null) {
               synchronized(processor) {
                   processor.waiting();
               }
           } else {
               // do stuff here
           }
       }
   }
}

Thanks

P.S.  At some point, I'm going to want to use the worker thread to
update a progress bar - I don't know if that'll affect anything or if
there's anything additional I will need to think about.
Tom Hawtin - 28 Mar 2007 23:57 GMT
> The issue I am having, is I don't understand how wait and notify apply
> in this case.  I was initially attempting to use the FileProcessor
> object as a lock.

The object that is used as a lock isn't really relevant. However, it's a
good idea to keep as much as possible private. That includes locks. So,
IMO it's generally best to use a dedicated lock object. As a little
trick you can use a nested (or local) class with the name Lock, to help
in stack dumps (more later). So:

    private static class Lock { }
    private final Object lock = new Lock();

>                    When the FileWorkerThread started up, if nothing
> was in the queue of the FileProcessor, the FileWorkerThread was
> supposed to wait until there was something in the queue.
> Unfortunately, when I called wait() on the FileProcessor object (the
> one that I was synchronizing over) the GUI would hang.

The way to start to diagnose these problems is with a dump of all the
thread stacks. Type ctrl-\ (or ctrl-break in Windows) from the console,
use the jstack tool in recent JDKs or use a debugger.

If you want a blocking Queue, there is a ready made, high performance
solution in the form of java.util.concurrent.BlockingQueue and its
implementations.

>     public synchronized void notifyWorker() {
>         this.notify();
>     }

This is an obvious problem. You should set some state within the
synchronized block and then notify (or actually better is the other way
around).

Also you want to consider could this ever be used in a situation where
there is more than one thread waiting. If the answer is yes, or not
really sure, use notifyAll.

>     public synchronized void waiting() {
>         try {
[quoted text clipped - 4 lines]
>         }
>     }

Again, the notify should only happen after some state change, and wait
should be in a while loop.

>         // work forever
>         while (true) {
[quoted text clipped - 9 lines]
>             }
>         }

There appears to be a race condition here. What happens if between the
dequeueFile and the synchronized, a file is queued?

Generally loops like this should look, for a non-blocking queue
implementation, something like:

        for (;;) {
            final File file;
            synchronized (lock) {
                while (queue.isEmpty()) {
                    lock.wait();
                }
                file = queue.poll();
            }
            ... do stuff with file ...
        }

> P.S.  At some point, I'm going to want to use the worker thread to
> update a progress bar - I don't know if that'll affect anything or if
> there's anything additional I will need to think about.

You should be in the Event Dispatch Thread to update any controls. Use
java.awt.EventQueue.invokeLater.

Tom Hawtin
blmblm@myrealbox.com - 29 Mar 2007 03:44 GMT
> > The issue I am having, is I don't understand how wait and notify apply
> > in this case.  I was initially attempting to use the FileProcessor
[quoted text clipped - 22 lines]
> solution in the form of java.util.concurrent.BlockingQueue and its
> implementations.

This might also simplify the whole design.  If you look at the API
documentation (javadocs) for this class, it gives a very simple
producer/consumer example that I think is similar to what the OP
wants to do.  All the wait() and notify() calls are buried in the
library code.  Using a blocking queue would have been my first
thought about how to approach this problem, and how nice that there's
now a library implementation -- several in fact.

[ snip lots of excellent advice that the OP should review if he'd
rather stick closer to his initial plan ]

Signature

B. L. Massingill
ObDisclaimer:  I don't speak for my employers; they return the favor.

Gordon Beaton - 29 Mar 2007 06:59 GMT
> This is an obvious problem. You should set some state within the
> synchronized block and then notify (or actually better is the other
> way around).

I'm curious - why is it better to notify *before* setting the state?

Intuitively, setting before notifying is the correct order, even
though the synchronization effectively removes the importance of order
here.

/gordon

--
Tom Hawtin - 29 Mar 2007 10:46 GMT
>> This is an obvious problem. You should set some state within the
>> synchronized block and then notify (or actually better is the other
[quoted text clipped - 5 lines]
> though the synchronization effectively removes the importance of order
> here.

Usually it's not particularly important, and it's very rare to see it done.

The usual way to think about synchronized block execution is that one
statement is executed after another until the end of block is reached.
However there are other ways of leaving the block. return, break and
continue of course. Hopefully you should spot those.

The other way to leave a synchronized block is through an exception. If
the state has been modified such that another thread should wakeup, you
will want to have notified. Theoretically even the call to notify()
could cause an exception.

More often than not, the state update is simple and any theoretical risk
can be, relatively safely, swept under the carpet.

Tom Hawtin
blmblm@myrealbox.com - 29 Mar 2007 22:00 GMT
> >> This is an obvious problem. You should set some state within the
> >> synchronized block and then notify (or actually better is the other
[quoted text clipped - 20 lines]
> More often than not, the state update is simple and any theoretical risk
> can be, relatively safely, swept under the carpet.

Seems to me that maybe the important thing is whether you want to
always perform the notify(), even if there are exceptions or errors,
or sometimes perform it and sometimes not, and that putting it first
would be appropriate for the first case but not for the second.  Or
am I missing the point you're making?

Signature

B. L. Massingill
ObDisclaimer:  I don't speak for my employers; they return the favor.

Tom Hawtin - 30 Mar 2007 01:39 GMT
> Seems to me that maybe the important thing is whether you want to
> always perform the notify(), even if there are exceptions or errors,
> or sometimes perform it and sometimes not, and that putting it first
> would be appropriate for the first case but not for the second.  Or
> am I missing the point you're making?

Why would you not want to perform a notify?

If there is any chance that something is updated that needs a notify,
then you should notify. Given that you can't (technically) guarantee to
notify after the update, doing it before isn't a bad idea (other than
causing other programmers to scratch their head).

Tom Hawtin
blmblm@myrealbox.com - 30 Mar 2007 01:52 GMT
> > Seems to me that maybe the important thing is whether you want to
> > always perform the notify(), even if there are exceptions or errors,
[quoted text clipped - 8 lines]
> notify after the update, doing it before isn't a bad idea (other than
> causing other programmers to scratch their head).

Ohhhh ....  Right.  Because the thing being notified is supposed to
check, after being waked up, whether the condition it was waiting
for is actually true.

So there's no harm in doing an extra notify, aside from a small
possible loss of efficiency.

"What was I thinking?"  ?

Signature

B. L. Massingill
ObDisclaimer:  I don't speak for my employers; they return the favor.

Jason Cavett - 29 Mar 2007 16:08 GMT
> > The issue I am having, is I don't understand how wait and notify apply
> > in this case.  I was initially attempting to use the FileProcessor
[quoted text clipped - 86 lines]
>
> Tom Hawtin

The BlockingQueue works very well, thank you.  Of course, as always, I
run into other problems (now I can't copy the ProjectModel because
BlockingQueue's aren't serializable, doh) but at least you've sent me
down a correct path.

I'll look up invokeLater.  I figured there was some way to sync the
thread and the GUI.  My issue revolves around how I "hook" the GUI in
with the thread that's running.  More of a design issue (which would
be too complicated for me to address here) than anything.

Thanks for your help.  It is much appreciated.
blmblm@myrealbox.com - 29 Mar 2007 03:35 GMT
> I will be the first to admit I don't *really* understand threads.
> Have a general idea, but, as I haven't had to use them too often, I
[quoted text clipped - 25 lines]
> same exact object as above - it's passed in on the FileWorkerThread's
> constructor).

Nitpick:  Maybe it would be clearer to call this a FileWorker, so it's
clear that's not actually a type of thread, but just something to hold
the run() method.

> The issue I am having, is I don't understand how wait and notify apply
> in this case.  I was initially attempting to use the FileProcessor
[quoted text clipped - 9 lines]
> FWT does the work -> When no more items are in the queue, FWT waits
> until it receives another notify

My first thought would be use a blocking queue.  More in another
post, since it seems that someone else has also replied suggesting
that.

> Here is the code for the FileProcessor and the FileWorkerThread:
>
[quoted text clipped - 13 lines]
>         workerThread.run();
>     }

Probable "gotcha":  I think you mean start() rather than run() --
run() executes the right code, but in the wrong thread --
in the thread calling the FileProcessor constructor rather
than in the thread you just created.

>     public synchronized void enqueueFile(File file) {
>         files.add(file);
[quoted text clipped - 66 lines]
> update a progress bar - I don't know if that'll affect anything or if
> there's anything additional I will need to think about.

Signature

B. L. Massingill
ObDisclaimer:  I don't speak for my employers; they return the favor.

Jason Cavett - 29 Mar 2007 14:26 GMT
On Mar 28, 10:35 pm, blm...@myrealbox.com <blm...@myrealbox.com>
wrote:
> In article <1175121023.111583.251...@n59g2000hsh.googlegroups.com>,
>
[quoted text clipped - 153 lines]
>
> - Show quoted text -

> Probable "gotcha":  I think you mean start() rather than run() --
> run() executes the right code, but in the wrong thread --
> in the thread calling the FileProcessor constructor rather
> than in the thread you just created.

Whoops.  My mistake.  I saw that after I posted to the code here.
Thanks for the reminder, though.
blmblm@myrealbox.com - 29 Mar 2007 22:04 GMT
> On Mar 28, 10:35 pm, blm...@myrealbox.com <blm...@myrealbox.com>
> wrote:

[ snip ]

> > Probable "gotcha":  I think you mean start() rather than run() --
> > run() executes the right code, but in the wrong thread --
[quoted text clipped - 3 lines]
> Whoops.  My mistake.  I saw that after I posted to the code here.
> Thanks for the reminder, though.

Sure.  It's a common newbie mistake, and I wasn't sure from your
post that you knew / remembered the right way.

By the way, your post as it showed up in my newsreader contained
the full text of the post to which you were replying, *plus* the
"Probably gotcha" lines included above.  It doesn't seem like that
could have been intentional.  Thinko, or glitch in whatever you
use to post?

Signature

B. L. Massingill
ObDisclaimer:  I don't speak for my employers; they return the favor.

Jason Cavett - 29 Mar 2007 22:32 GMT
On Mar 29, 5:04 pm, blm...@myrealbox.com <blm...@myrealbox.com> wrote:
> In article <1175174776.352639.32...@y80g2000hsf.googlegroups.com>,
>
[quoted text clipped - 23 lines]
> B. L. Massingill
> ObDisclaimer:  I don't speak for my employers; they return the favor.

Really?  Weird.  I wonder if that's because I had that part
highlighted when I hit "Reply."  I'll have to look into that.  Thanks.
SadRed - 29 Mar 2007 04:39 GMT
> I will be the first to admit I don't *really* understand threads.
> Have a general idea, but, as I haven't had to use them too often, I
[quoted text clipped - 129 lines]
> update a progress bar - I don't know if that'll affect anything or if
> there's anything additional I will need to think about.

/*
  If you use java.util.concurrent.BlockingQueue object for enqueueing/
dequeueing of Files, or application's other principal resources, wait/
notify is automatically handled in the object. You don't need to worry
about anything for them.

  As a programming exercise, however, you should do wait/notify *on
the methods of data structure object itself*, <b>not on its user
objects</b>. Your current code violates this most important principle.

  I hope you try and study this code.
  (Local file system access can't be but sequential. So, multi-
threading on
  them is futile, though.)
*/
import java.io.*;
import java.util.*;

public class FileProcessorDriver{

 public static void main(String[] args){

   File[] files = new File(".").listFiles(new FilenameFilter(){
     public boolean accept(File dir, String name){
       return (name.endsWith(".txt"));  // text file only
     }
   });

   FileProcessorX fp = new FileProcessorX(files);

   FileWorker fw1 = new FileWorker(fp, "FW1");
   FileWorker fw2 = new FileWorker(fp, "FW2");
   FileWorker fw3 = new FileWorker(fp, "FW3");
   fw1.start();
   fw2.start();
   fw3.start();
 }
}

class FileProcessorX{
 static final int CAPACITY = 100;
 Vector<File> files;
 private int fnum;

 public FileProcessorX(File[] fs) {
   files = new Vector<File>(Arrays.asList(fs));
   fnum = files.size();
 }

 public synchronized void enqueueFile(File file) {
   while (fnum >= CAPACITY){ // saturated
     try{
       wait();
     }
     catch (InterruptedException e){
       e.printStackTrace();
     }
   }
   ++fnum;
   files.add(file);
   notifyAll();  // wake up ones who were waiting on dequeueFile
 }

 public synchronized File dequeueFile() {
   while (files.isEmpty()){
     try{
       wait();
     }
     catch (InterruptedException e){
       e.printStackTrace();
     }
   }
   --fnum;
   notifyAll();  // wake up ones who were waiting on enqueueFile
   return files.remove(0);
 }
}

class FileWorker extends Thread{
 FileProcessorX fpx;
 Vector<File> fvec;
 String mark;

 public FileWorker(FileProcessorX fp, String mk){
   fpx = fp;
   mark = mk;
 }

 public void run(){
   while (true){
     File f = fpx.dequeueFile();
     work(f);
   }
 }

 void work(File file){
   String line = null;

   try{
     BufferedReader br = new BufferedReader(new FileReader(file));
     while ((line = br.readLine()) != null){
       System.out.println(mark + " : " + line);
     }
   }
   catch (Exception e){
     e.printStackTrace();
   }
 }
}
Patricia Shanahan - 29 Mar 2007 05:03 GMT
...
>    (Local file system access can't be but sequential. So, multi-
> threading on
>    them is futile, though.)
...

Why must local file system access be sequential?

Patricia
SadRed - 29 Mar 2007 05:17 GMT
> ...>    (Local file system access can't be but sequential. So, multi-
> > threading on
[quoted text clipped - 5 lines]
>
> Patricia

Oh sorry. I didn't know that you use a multi-head hard disk device!
Patricia Shanahan - 29 Mar 2007 05:31 GMT
>> ...>    (Local file system access can't be but sequential. So, multi-
>>> threading on
[quoted text clipped - 6 lines]
>
> Oh sorry. I didn't know that you use a multi-head hard disk device!

You still seem to be implying that local file system access is limited
to one device. I've worked with systems that had dozens of disk drives.

However, even if there is only one disk head, getting multiple I/O's
issued can enable disk head movement optimization. If the I/Os are
issued one at a time, the drive has to do them in that order, which is
not necessarily an efficient order.

Patricia
SadRed - 29 Mar 2007 06:12 GMT
> >> ...>    (Local file system access can't be but sequential. So, multi-
> >>> threading on
[quoted text clipped - 17 lines]
> Patricia
> I've worked with systems that had dozens of disk drives.
And they had supported parallel I/Os. How gorgeous!

> multiple I/O's
> issued can enable disk head movement optimization
The most optimized disk head movement is NOT hopping
around multiple sectors spread over the platter. Anyway,
I believe there's no such thing as disk head movement
optimization that could support a pseudo-parallel, sip
by sip reads/writes. And even if there were, oveall
performance would be almost same as pure serial access.
Disk device used to be an awkward machinery, after all.

As a theoretical hypotheses, we exclude the use of
memory mapped file I/O for this particular forum thread.
Patricia Shanahan - 29 Mar 2007 09:53 GMT
>>>> ...>    (Local file system access can't be but sequential. So, multi-
>>>>> threading on
[quoted text clipped - 14 lines]
>> I've worked with systems that had dozens of disk drives.
> And they had supported parallel I/Os. How gorgeous!

I was one of the developers of the Sun E10000. With up to 64 processors,
we needed far higher I/O bandwidth and transaction rate than we could
have got out of a single I/O path. I don't think we ever even thought
about sequential local file system access as a design option.

Patricia
SadRed - 29 Mar 2007 10:09 GMT
> >>>> ...>    (Local file system access can't be but sequential. So, multi-
> >>>>> threading on
[quoted text clipped - 21 lines]
>
> Patricia

Wow 64 pocessors! Good. You are an elite and we are not. We are not
talking about your level of system in every topic of public fora. You
even don't understand that but you should.
Christian - 29 Mar 2007 12:13 GMT
SadRed schrieb:
>>>>>> ...>    (Local file system access can't be but sequential. So, multi-
>>>>>>> threading on
[quoted text clipped - 22 lines]
> talking about your level of system in every topic of public fora. You
> even don't understand that but you should.

and even in a normal system I would say it is not the job of the
programmer to handle that disc access is parallel or seriell...

at least not if its only a few parallel accesses.

Its the job of the OS to perform IO and properly cache disc reads... and
the disc's cache should also be helping...

If you really want to help your os .. well use a large ByteBuffer..

though the example stays .. there are things that must be accessed
sequential.. use  wait/notify there..
Lew - 29 Mar 2007 13:21 GMT
> Wow 64 pocessors! Good. You are an elite and we are not. We are not
> talking about your level of system in every topic of public fora. You
> even don't understand that but you should.

Attacking the speaker does not invalidate the claim.

-- Lew
Patricia Shanahan - 29 Mar 2007 15:19 GMT
>>>>>> ...>    (Local file system access can't be but sequential. So, multi-
>>>>>>> threading on
[quoted text clipped - 22 lines]
> talking about your level of system in every topic of public fora. You
> even don't understand that but you should.

I'm afraid you are right that I don't understand the idea of separating
computing into "elite" and "non-elite" categories, with only the
"non-elite" considered fit for discussion in public fora.

Out of order execution was one of the special features of the CDC 6600,
presumably "elite". These days, it is normal in desktop processors.

RAID was originally a way of applying to servers the price/capacity of
mass produced disk drives, but the last time I saw a terabyte scale
RAID-5 disk array it was on the external disk drive shelf at a local
home electronics store.

A few years ago someone who divided computing into "elite" and
"non-elite" might have classed parallelizing compute bound work as an
"elite" technique, because, of course, no computer one should talk about
in public fora could possibly really run more than one thread at a time.

My working assumption is that there are three possible states for an
"elite" computer performance idea: it is already on desktops, or it is
coming soon to a desktop near you, or it has been superseded by better
ideas.

Patricia
Lew - 29 Mar 2007 23:26 GMT
> A few years ago someone who divided computing into "elite" and
> "non-elite" might have classed parallelizing compute bound work as an
[quoted text clipped - 5 lines]
> coming soon to a desktop near you, or it has been superseded by better
> ideas.

Indeed, "X2" multi-core processors are The Thing now at very reasonable
prices, and SMP versions of Windows and Linux are commonly available.

Late model Intel chips also have that lovely "hyper-threading" that lets one
take advantage of SMP as well.

-- Lew
Martin Gerner - 27 Apr 2007 19:46 GMT
> I'm afraid you are right that I don't understand the idea of
> separating computing into "elite" and "non-elite" categories, with
[quoted text clipped - 21 lines]
>
> Patricia

Sorry about the spam, but I just have to say that I really loved this
post of yours :)

Haven't been smiling so much for a while, I believe.

Thanks for all the great posts and advice you give to this group!

Signature

Martin Gerner

John W. Kennedy - 29 Mar 2007 19:49 GMT
>>>> ...>    (Local file system access can't be but sequential. So, multi-
>>>>> threading on
[quoted text clipped - 14 lines]
>> I've worked with systems that had dozens of disk drives.
> And they had supported parallel I/Os. How gorgeous!

Real computers have been able to do that since the early 60s.

>> multiple I/O's
>> issued can enable disk head movement optimization
[quoted text clipped - 4 lines]
> by sip reads/writes. And even if there were, oveall
> performance would be almost same as pure serial access.

So much for nearly half a century of computer science and
operating-system design.

There's more to the world than bog-standard personal computers, you
know, and there's more to performance optimization than your offhand
guesses.

Signature

John W. Kennedy
"Sweet, was Christ crucified to create this chat?"
  -- Charles Williams.  "Judgement at Chelmsford"
* TagZilla 0.066 * http://tagzilla.mozdev.org

SadRed - 30 Mar 2007 00:20 GMT
> >>>> ...>    (Local file system access can't be but sequential. So, multi-
> >>>>> threading on
[quoted text clipped - 38 lines]
>    -- Charles Williams.  "Judgement at Chelmsford"
> * TagZilla 0.066 *http://tagzilla.mozdev.org

As my last post on this thread I'd like to restate that unless you
have a combination of very special hardwares and a very special
operating system, you can't get a real parallelism from your disk-
based file system. Period.
Lew - 30 Mar 2007 01:48 GMT
> As my last post on this thread I'd like to restate that unless you
> have a combination of very special hardwares and a very special
> operating system, you can't get a real parallelism from your disk-
> based file system. Period.

One doesn't necessarily need "true parallelism" to benefit from things like
elevator-seeking algorithms, which can benefit from thread parallelism.

All you need are IDE hard drives - one can issue parallel operations to disks
on the primary and secondary IDE ports - and a consumer OS like MS Windows or
Linux, which have been doing so for quite awhile.

Just to pick one example of the sort of combination of very special hardware
and a very special operating system that one might encounter in the wild.

-- Lew


Free Magazines

Get these publications absolutely FREE for up to 12 months. There are no hidden fees and no obligation. Simply choose a title, complete the application form and submit it. Read more ...

Oracle MagazineNetwork ComputingComputer WorldBio-IT WorldeWeekInformation WeekInfosecurity
 
Sign In
Join
My Latest Posts
My Monitored Threads
My Blog
My Photo Gallery
My Profile
My Homepage

Start New Thread
Enable EMail Alerts
Rate this Thread



©2009 Advenet LLC   Privacy Policy - Terms of Use
This website includes both content owned or controlled by Advenet as well as content owned or controlled by third parties.