There are two ways to define, create and run threads. One is to extend from the Thread class, overriding the default run() method with another implementation. The other is to implement the Runnable interface, which only contains one method run(), and put in the run() code. With a Runnable implementor, a Thread instance needs to be created with the implementor in the argument, allowing the code to be threaded.
When a thread is created but not started using the start() method, it is in the new state. Once started, it will graduate to the runnable state and enters the runnable pool where threads wait for the scheduler to pick them and run them. The run order of runnable threads is not guaranteed. What is guaranteed is that each thread will start and each thread will run to completion. The running state is what all threads aspire to. However, a thread may be pulled off by the waiting, blocking or sleeping states (If the scheduler does not pull it off first). In which case, the thread would still be alive but not eligible for running. Once a thread's run() method is done, the thread is dead. And once the thread has been started, it can never be started again (Runtime exception).
Thread priorities are positive integers ranging from 1 to 10, though it is better to use the constants Thread.MIN_PRIORITY, Thread.NORM_PRIORITY and Thread.MAX_PRIORITY. The scheduler guarantees that the thread running will always have a greater or equal priority to the highest priority threads in the runnable pool.
Race condition is a problem where multiple threads vie for access to a single object and in the process, corrupt the state of the object, producing data which is of no use to any of the threads.
The solution to this is to lock methods or code blocks so that only one thread can access the object. Only code that can change the object needs be synchronized. Threads trying to get the object's lock, but discover it has already been taken, is said to be blocked on the object's lock. They go into a pool for that object and wait until the lock is released, whereby any of them can get it. Care has to be taken that deadlock does not occur. Deadlock is when two threads are blocked, with each waiting for the other's lock.
wait(), notify() and notifyAll() are methods that deal with objects' locking status. These methods can be called on any objects, within a synchronized context. A thread cannot call these methods on an object unless it owns that object's lock. The thread that calls the wait() method on an object goes into a waiting room, i.e. sleeps, until the notify()/notifyAll() method is invoked on the same object. Thus wait() and notify() must be used on the same object.