This is actually a very complex issue. A few guidelines:
The init() method is guaranteed to be called once per servlet instance, when the servlet is loaded. You don’t have to worry about thread safety inside this method, since it is only called by a single thread, and the web server will wait until that thread exits before sending any more threads into your service() method.
Every new client request generates (or allocates) a new thread; that thread calls the service() method of your servlet (which may in turn call doPost(), doGet() and so forth).
Under most circumstances, there is only one instance of your servlet, no matter how many client requests are in process. That means that at any given moment, there may be many threads running inside the service() method of your solo instance, all sharing the same instance data and potentially stepping on each other’s toes. This means that you should be careful to synchronize access to shared data (instance variables) using the synchronized keyword.
(Note that the server will also allocate a new instance if you register the servlet with a new name and, e.g., new init parameters.)
Note that you need not (and should not) synchronize on local data or parameters. And especially you shouldn’t synchronize the service() method! (Or doPost(), doGet() et al.)
A simple solution to synchronizing is to always synchronize on the servlet instance itself using "synchronized (this) { … }". However, this can lead to performance bottlenecks; you’re usually better off synchronizing on the data objects themselves.
If you absolutely can’t deal with synchronizing, you can declare that your servlet "implements SingleThreadModel". This empty interface tells the web server to only send one client request at a time into your servlet. From the JavaDoc: "If the target servlet is flagged with this interface, the servlet programmer is guaranteed that no two threads will execute concurrently the service method of that servlet. This guarantee is ensured by maintaining a pool of servlet instances for each such servlet, and dispatching each service call to a free servlet. In essence, if the servlet implements this interface, the servlet will be thread safe." Note that this is not an ideal solution, since performance may suffer (depending on the size of the instance pool), plus it’s more difficult to share data across instances than within a single instance.
See also What’s a better approach for enabling thread-safe servlets and JSPs? SingleThreadModel Interface or Synchronization?
To share data across successive or concurrent requests, you can use either instance variables or class-static variables, or use Session Tracking.
The destroy() method is not necessarily as clean as the init() method. The server calls destroy either after all service calls have been completed, or after a certain number of seconds have passed, whichever comes first. This means that other threads might be running service requests at the same time as your destroy() method is called! So be sure to synchronize, and/or wait for the other requests to quit. Sun’s Servlet Tutorial has an example of how to do this with reference counting.
destroy() can not throw an exception, so if something bad happens, call log() with a helpful message (like the exception). See the "closing a JDBC connection" example in Sun’s Tutorial.