Two threads deadlocking but can't see why, lock released with notifyAll() - java

using JConsole it seems i get a deadlock situation when 2 threads try to modify this object.
package com.steven.concurrent.assignment2.memoryallocator;
/*
* This seems to deadlock... cant see why though.
*/
public class MemAllocMonitor implements IMemoryAllocator {
private final int MAX_FREE = 50;
private int freePages = MAX_FREE;
//I think this would work, without even the need for sync blocks.....
// But only in the situaion where i would not have to check the bounds of the updates. If it was just modification, this would be
// fine....
//private volatile int freePages = 50;
public MemAllocMonitor(int pages){
assert(pages < MAX_FREE);
this.freePages = pages;
}
public MemAllocMonitor(){
}
#Override
public synchronized void request(int number) {
if(number < 0)
throw new IllegalArgumentException();
while(freePages - number < 0) {
System.out.println("No space....waiting...");
try {
this.wait();
} catch (Exception e) {}
}
freePages -= number;
System.out.println("Requested : " + number + " remaining " + freePages);
this.notifyAll();
}
#Override
public synchronized void release(int number) {
if(number < 0)
throw new IllegalArgumentException();
while(freePages + number > MAX_FREE) {
System.out.println("page table full....would be " + (number + freePages) );
try {
this.wait();
} catch (Exception e) {}
}
freePages += number;
System.out.println("Released : " + number + " remaining " + freePages);
this.notifyAll();
}
#Override
public int getFreePages() {
return freePages;
}
}
This object is accessed via a simple wrapper that implements runnable, and calls either method as shown below.
package com.steven.concurrent.assignment2.memoryallocator;
import concurrent.RandomGenerator;
import concurrent.Time;
public class MemAllocRequester implements Runnable, MemoryAllocatorAction{
private IMemoryAllocator memoryAllocator;
private volatile boolean shutdown = false;;
public MemAllocRequester(IMemoryAllocator memAlloc){
this.memoryAllocator = memAlloc;
}
#Override
public void run() {
while(!shutdown){
Time.delay(500);
memoryAllocator.request(RandomGenerator.integer(0, 30));
}
}
public void ShutDown(){
this.shutdown = true;
}
}
and
package com.steven.concurrent.assignment2.memoryallocator;
import concurrent.RandomGenerator;
import concurrent.Time;
public class MemAllocReleaser implements Runnable, MemoryAllocatorAction{
private IMemoryAllocator memoryAllocator;
private volatile boolean shutdown = false;;
public MemAllocReleaser(IMemoryAllocator memAlloc){
this.memoryAllocator = memAlloc;
}
#Override
public void run() {
while(!shutdown){
Time.delay(500);
memoryAllocator.release(RandomGenerator.integer(0, 30));
}
}
public void ShutDown(){
this.shutdown = true;
}
}
It is started off as such...
package com.steven.concurrent.assignment2.memoryallocator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MemAllocMain {
public static void main(String[] args){
ExecutorService executor = Executors.newFixedThreadPool(10);
//IMemoryAllocator memoryAllocator = new MemAllocSemaphore();
IMemoryAllocator memoryAllocator = new MemAllocMonitor();
System.out.println("Starting app with " + memoryAllocator.getFreePages() + " pages...");
Thread t1 = new Thread(new MemAllocRequester(memoryAllocator));
Thread t2 = new Thread(new MemAllocReleaser(memoryAllocator));
t1.setName("MEMORY REQUESTER £££££££££££££££££££");
t2.setName("MEMORY RELEASER £££££££££££££££££££");
executor.submit(t1);
executor.submit(t2);
}
}
I have implemented a solution using the semaphore class, but for some reason this is causing trouble using the default java monitor solution. It runs for about 30 seconds, then both threads go into their waiting state, even though the lock should be enforced.

The problem is that both threads are hitting the upper and lower bounds (50 and 0 respectively) at the same time. Both examples below highlight the deadlock.
Scenario 1
request(29) - freePages=21
request(30) - under 0 so waits
release(30) - over 50 so waits : deadlock
Scenario 2
request(29) - freePages=21
release(30) - over 50 so waits
request(30) - under 0 so waits : deadlock
I am not sure what the exact requirements are for the homework problem but you need to revisit the release and request methods. I see two viable solutions:
Change the release method so that it only releases up to MAX_FREE but will still return
Change the release method so that it can release a subset of the amount requested, notifyAll, reenter the wait so it can release the remaining amount.
Also, you are kind of using the ExecutionService wrong. The ExecutionService is what creates the Threads so there is no reason for you to create the threads like you are doing.
Thread t1 = new Thread(new MemAllocRequester(memoryAllocator));
Thread t2 = new Thread(new MemAllocReleaser(memoryAllocator));
The threads you are creating will actually never be 'started' as Threads. It is still working for you because the ExecutionService threads will call your Thread.run() which will call MemAlloc*.run(). i.e. your t1 and t2 threads just pass the run() call along and provide no value.
Your MemAllocRequester and MemAllocReleaser are Runnables so just pass those into the ExecutionService directly.
executor.submit(new MemAllocRequester(memoryAllocator));
executor.submit(new MemAllocReleaser(memoryAllocator));

Related

How do I get my threads to run in an orderly sequence?

I'm learning Java threads and want my code to output threads 0-9 in sequential order. I used the synchronized keyword but I don't get the results I expect.
What should I do to correct my code?
public class MyThread extends Thread {
private static final int threadMax = 10;
private static int runCount = 0;
public void printThread() {
synchronized (this) {
while (runCount++ < 100) {
System.out.println(runCount + ": " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public void run() {
printThread();
}
public static void main(String[] args) {
for (int i = 0; i < threadMax; i++) {
new MyThread().start();
}
}
}
It is not working as every time you are creating new MyThread object and you are synchronized over that new object. So, every Thread you created will get a lock on the diffrent object. So, you should pass a common object to take the lock like below.
class MyThread extends Thread {
private static int runCount = 0;
Object lock;
public MyThread(Object lock) {
this.lock = lock;
}
public void printThread() {
synchronized (lock) {
// your code here
}
}
//.........
}
And then call it like :
Object lock = new Object();
for (int i = 0; i < threadMax; i++) {
new MyThread(lock).start();
}
However, the above program will not ensure you that it will run in sequence. There are several ways to do that. You can use wait() and notify() to achieve your goal. Refer the below example :
public void printThread() {
while (runCount < 90) {
synchronized (lock) {
while (runCount % 10 != remainder) {
try {
lock.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(runCount + ": " + Thread.currentThread().getName());
runCount++;
lock.notifyAll();
}
}
}
And call the thread like :
Object lock = new Object();
for (int i = 0; i < 10; i++) {
new MyThread(lock, i).start();
}
You are synchronizing the context of the thread, which is different for each one. You should put into the synchronized key any common object for all diferent threads. This won't make them run in any certain secuence, just to wait each other to end.
If you want to test the synchronized keyword for any purpose, you could pass the constructor a common variable and use it in every thread:
public class MyThread extends Thread {
private static final int threadMax = 10;
private static int runCount = 0;
private Object test; //Object pointing main method
public MyThread(Object test){
this.test = test; //This won't copy values as it is an object and not a number, string...
}
public void printThread() {
synchronized (test) { //Same object for all threads
while (runCount++ < 100) {
System.out.println(runCount + ": " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public void run() {
printThread();
}
public static void main(String[] args) {
Object test; //common object
for (int i = 0; i < threadMax; i++) {
new MyThread(test).start();
}
}
}
If you want also to make them start in order, you should "synchronize" the loop making wait and notify calls.
Anyway, the point about multithreading is to have several threads running at the "same" time and not in sequence, as that would be the same as a linear execution.
You have several tasks that you want to delegate to threads but have them executed sequentially.
As others have pointed out, wait & notify can help you achieve that : wait until Nth have finished then notify the next. However, if you wait/notify inside your printThread method, as all your threads are waiting simultaneously on the same lock, there is no guaranties that N+1th thread will be next. So you may have
1: thread-1
...
10: thread-1
11: thread-5
...
20: thread-5
21: thread-2
...
If that's ok for you, you're done. However, in a situation where you specifically want your threads to be ordered, what you need is a waiting queue (FIFO : First In First Out).
To achieve that, you can use the awesome ExecutorService. Be aware however that they hide the Threads from you and picking that solution should not be at the cost of understanding the basics of them beforehand.
An ExecutorService is a very convenient class that can receive tasks (in the form of a Runnable, see below) and will execute them in separate Threads.
Here, I'm using a SingleThreadExecutor which execute the submitted tasks sequentially. So all you have to do is call it's execute method with your tasks as arguments, and the ExecutorService will run them in the right order, one after the other.
Here's what you can do with a few notes :
public class ThreadRunner {
// Note : Constants are usually all uppercase in Java
private static final int MAX_THREADS = 10;
private final int threadName;
public ThreadRunner(int threadName) {
this.threadName = threadName;
}
public void printThread() {
// Note: For loops are better than while when you already know the number of iterations
for (int runCount = 0; runCount < 10; runCount++) {
System.out.println(runCount + "th run from thread " + threadName);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < MAX_THREADS; i++) {
int threadName = i + 1;
// Submit a task to the executor
executorService.execute(() -> new ThreadRunner(threadName).printThread());
}
// Nicely ask for the executor to shutdown.
// Then wait for already submitted tasks to terminate.
executorService.shutdown();
try {
executorService.awaitTermination(120, TimeUnit.SECONDS);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
I changed a few details, here are the reasons :
Thread creation : don't inherit from Thread
I would advise you not to inherit from Thread, but create a local instance of it, as all you need is to use a Thread ; you don't want to be a Thread :
public static void main(String[] args) {
// Using Java 1.8+ lambda
Thread lambdaThread = new Thread(() -> System.out.println("Hello from a lambda in a Thread"));
lambdaThread.start();
// Using an anonymous class for java <1.8
Thread anonClassThread = new Thread(new Runnable() {
#Override
public void run() {
System.out.println("Hello from an anonymous class in a Thread");
}
});
anonClassThread.start();
}
You're creating a new Thread passing a Runnable as constructor argument, using either lambda or anonymous class, depending of your Java version.
A Runnable is simply a portion of code that will be executed (by a Thread, in this case).
Same apply to ExecutorService, it's execute methode takes a Runnable which I've created through lambdas.
Sharing static counter between threads
Your line private static int runCount = 0; is a static field, which means it is shared by all instances of the class MyThread. When you increase it in a thread, all threads will read (and write) to the same variable.
If your threads were running sequentially, the first would do it's 100 iterations, then when the second thread starts, runCount is already at 100 and you're not entering your while loop. If that wasn't intended, it may be confusing when you'll test your code.
Based on your expected output in a comment, I believe you want your threads to do 10 iterations each, not share a pool of 100 iterations and manage somehow to have each of them only perform 10.
Having the name of the thread belong to each ThreadRunner
Small detail here : previously, you were creating 10 threads. Here, the ExecutorService only creates one that he reuse for each task you submit. So Thread.currentThread().getName() would always be thread-1.
You wouldn't be able to see which task is running without this field.
If each task is started after the previous, you don't need 10 Threads, but a single Thread performing the 10 tasks sequentially.
I've been as complete as possible, but some points might be a little bit tricky, so don't hesitate to ask for clarifications!

How can I start a service in a different thread without affecting debbuging? [duplicate]

I'm having a strange issue with multiple threads and breakpoints in IntelliJ IDEA 14.0.2. Code after the breakpoint is executed before it stops on it.
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
private static final int NUM_CLIENTS = 1000;
static class TestRunnable implements Runnable {
AtomicInteger lock;
#Override
public void run() {
synchronized (this.lock) {
int curCounter = this.lock.addAndGet(1);
System.out.println("Thread: " + Thread.currentThread().getName() + "; Count: " + curCounter);
if (curCounter >= NUM_CLIENTS) {
lock.notifyAll();
}
}
}
}
public static void main(final String args[]) {
final AtomicInteger lock = new AtomicInteger(0);
for (int i = 0; i < NUM_CLIENTS; i++) {
TestRunnable tr1 = new TestRunnable();
tr1.lock = lock;
new Thread(tr1).start();
}
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main woken up");
}
}
}
When I put a breakpoint (Suspend All) at line 12, synchronized (this.lock), System.out.println still executes (sometimes several times). Here's a screenshot:
As far as I know, all threads should stop at the breakpoint.
The documentation reads confusingly, but this is the relevant block. What it distills down to is setting the property to suspend on threads, and not the entire application instead. This will cause you to hit the break point on each individual thread instead of an arbitrary, indeterminate thread.
Suspend Policy: All
When a breakpoint is hit, all threads are suspended.
Suspend Policy: Thread
When the breakpoint is hit, the thread where the breakpoint is hit is suspended.

How to terminate all other running threads after any of one thread is finish

Problem: I have collection of threads start in a loop parallelly. After exiting anyone of thread first ,all other running threads must be terminated. This is what I tried but it doesn't work. Any help is appreciated.
public class ThreadsMain {
public static void main(String[] args) {
int SIZE = 3;
Thread t[] = new Thread[SIZE];
for (int i = 0; i < SIZE; i++) {
myThreads th = new myThreads();
t[i] = new Thread(th);
t[i].start();
}
}
}
Here is one way to do it, with a synchronizer implemented with intrinsic locks, and using interruption to cancel the unfinished tasks. The data structure makes a consumer thread block until a producer has submitted a result, then it cancels the other worker threads.
This is a toy example, see the link at the end for the real-world way to do this.
First, here's a threadsafe data structure that accepts results, it allows threads to register as listeners and interrupts them once it has a result submitted to it:
class MyQueue<T> {
private java.util.List<T> results = new java.util.ArrayList<T>();
private java.util.List<Thread> listeners = new java.util.ArrayList<Thread>();
public synchronized void put(T o) {
results.add(o);
notifyAll();
for (Thread listener : listeners) {
listener.interrupt();
}
}
public synchronized T take() throws InterruptedException {
while (results.size() == 0) {
wait();
}
return results.remove(0);
}
public synchronized void addListener(Thread t) {
listeners.add(t);
}
}
(I don't like having this class know so much about the listeners but I don't want to overthink a toy example either.)
The wait method releases the lock and makes the calling thread go dormant until a notification occurs (or it can just stop waiting arbitrarily). It uses the size property of the results list to know when a result has been submitted. It's not safe to assume that because a thread stopped waiting that you can infer something about the current state, once the thread reacquires the lock it needs to check what the current state actually is. For more about how wait works see this tutorial.
Here's a task that calculates a result (sleeping between iterations just so these threads can run for a while):
class FibTask implements Runnable {
private final MyQueue<BigInteger> queue;
private final int n;
private long sleepTime;
public FibTask(int n, long sleepTime, MyQueue<BigInteger> queue) {
this.n = n;
this.sleepTime = sleepTime;
this.queue = queue;
}
#Override public void run() {
BigInteger a = BigInteger.valueOf(0);
BigInteger b = BigInteger.valueOf(1);
int i = 0;
try {
while (!Thread.currentThread().isInterrupted() && i < n) {
i = i + 1;
BigInteger temp = a;
a = b;
b = a.add(temp);
Thread.sleep(sleepTime);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
if (!Thread.currentThread().isInterrupted()) {
queue.put(b);
}
}
}
Notice in the code above how the Runnable needs to be aware of attempts to interrupt it. Interruption is cooperative, the task is responsible for deciding when to detect interruption and for handling the termination process.
Also if a task involves IO then in some cases interruption doesn't work and you have to close the socket, see this article for more discussion of this.
Here's the main program that runs the threads and gets the result. The MyQueue class is already doing most of the work so this doesn't have to do much:
class Completion {
public static void main(String ... args) throws Exception {
MyQueue<BigInteger> queue = new MyQueue<BigInteger>();
Thread t1 = new Thread(new FibTask(10, 1000L, queue));
Thread t2 = new Thread(new FibTask(20, 10000L, queue));
Thread t3 = new Thread(new FibTask(25, 50000L, queue));
queue.addListener(t1);
queue.addListener(t2);
queue.addListener(t3);
t1.start();
t2.start();
t3.start();
System.out.println(queue.take());
}
}
Be aware this isn't a fair race because of how the threads' starts are staggered, later threads are at a disadvantage. Submitting tasks to an Executor that initializes a threadpool up front would make sure that the time to start a thread didn't cause a delay here.
For a better way that makes use of java.util.concurrent features like Executors and Futures, see the example given in the API documentation for ExecutorCompletionService.
A simple approach, use a synchronized class to handle the loop condition:
class ThreadHandler
{
static Object lock = new Object();
static boolean finished = false;
static void finishThreads()
{
synchronized(lock)
{
finished = true;
}
}
static boolean isFinished()
{
boolean result;
synchronized(lock)
{
result = finished;
}
return result;
}
}
And in your runnable
class myThreads implements Runnable
{
#Override
public void run()
{
while(!ThreadHandler.isFinished())
{
}
}
}

Spurious wakeups on windows. Is it possible?

I recently learned "Spurious wakeups"
Any people say that this problem possible only for some types of Linux PC.
I use windows.
I wrote test for Spurious wakeups. I got result that it is possible. But I want to show this test for you. Maybe I made mistake somewhere.
my initial variant:
import java.util.Random;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class TestSpuriousWakeups {
static final int MAX_THREADS = 600;
static final Object mutex = new Object();
static final CountDownLatch allThreadsStarted =
new CountDownLatch(MAX_THREADS);
static final CountDownLatch allThreadsFinished =
new CountDownLatch(1);
static /*final*/ volatile AtomicInteger processedThreads = new AtomicInteger();
static /*final*/ volatile AtomicInteger notifiedThreads = new AtomicInteger();
final int n = 10;
static volatile boolean continueCondition = true;
static final Random sleepRandom = new Random();
static class Worker extends Thread {
public void run() {
try {
synchronized (mutex) {
allThreadsStarted.countDown();
mutex.wait();
}
continueCondition = true;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
processedThreads.incrementAndGet();
}
}
}
static class Notifier extends Thread {
public void run() {
while (true) {
if (processedThreads.get() == MAX_THREADS)
break;
synchronized (mutex) {
doStuff();
mutex.notify();
continueCondition = false;
notifiedThreads.incrementAndGet();
}
}
allThreadsFinished.countDown();
}
// just to emulate some activity
void doStuff() {
try { Thread.sleep(sleepRandom.nextInt(5)); }
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) throws Exception {
for (int i = 0; i < MAX_THREADS; i++)
new Worker().start();
// wait for all workers to start execution
allThreadsStarted.await();
new Notifier().start();
// wait for all workers and notifier to finish execution
allThreadsFinished.await();
System.out.println("Spurious wakeups count: "
+ (MAX_THREADS - notifiedThreads.get()));
}
}
4 random execution:
Spurious wakeups count: -20
Spurious wakeups count: -5
Spurious wakeups count: 0
Spurious wakeups count: -407
So different values is wondering for me.
I added pair of rows to run method:
static class Notifier extends Thread {
public void run() {
while (true) {
while (!continueCondition) //added string
doStuff(); //added string
// all threads finished their execution
if (processedThreads.get() == MAX_THREADS)
break;
synchronized (mutex) {
doStuff();
mutex.notify();
continueCondition = false;
notifiedThreads.incrementAndGet();
}
}
allThreadsFinished.countDown();
}
after it I cannot get something another than
Spurious wakeups count: 0
Is it really Spurious wakeups or bug in my experiment ?
P.S.
I noticed that I see negatives numbers. Thus obviously it is experiment bug. But I don't understand cause.
Two things
Spurious wake ups are real, even on Windows. This is documented in the WinAPI: http://msdn.microsoft.com/en-us/library/windows/desktop/ms682052(v=vs.85).aspx
You have a race condition in your test. So, I don't think it's quite accurate.
The race is between the exit of the synchronized block in your worker threads and when they reach processedThreads.incrementAndGet(). Notifier will spin during that time, notifying threads which may or may not have acquired the lock.
In other words
It's possible for Notifier to spin twice (that is, notify() twice) before a worker thread can acquire the mutex.
It's possible for Notifier to spin after the last thread has exited the synchronized block but not yet reached its finally block.
Your two added lines change the output because, by slowing down the Notifier, you're masking the race. (By giving Worker lots of time to enter the mutex.)
Hope that makes some sense.

Stuck in wait() for thread execution to return

I'm trying to write a thread that I can delegate testing and evolution of a robot to while I sort the existing chromosomes by fitness in the main thread. Below is the initial fitness method. What I want to do here is to have each genome tested by a robotHandler as the tests are 30 - 40 seconds long. I will only be running one of these threads at any given time.
Currently I seem to get caught in the wait() section of the intialFitness method. This is my first attempt at multithreading so any help as to how to debug the problem or if someone can spot the issue that would be fantastic
The RobotInterface class is just a testing class at the moment, I have commented out the log4j and sleep declarations to try and rule these out (Incidentally log4j was not logging anything in the thread if that helps)
public synchronized ArrayList<Genome> initialFitness( ArrayList<Genome> population)
{
for ( int i = 0; i < population.size(); i++ )
{
candidateTest = new CandidateTest(population.get(i));
Thread robotHandler = new Thread(new RobotInterface( candidateTest));
while(! (candidateTest.finishedYet() ))
{
try
{
wait();
}
catch (InterruptedException e)
{
logger.debug("The initialFitness method was interrupted, this shouldn't happen");
}
}
population.set(i, candidateTest.getCandidate());
}
return population;
}
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import java.util.Random;
The RobotInterface Class
public class RobotInterface implements Runnable
{
// create a serial connection
// transmit a string and check for response
// wait for evaluation
// take evaluation
private CandidateTest candidate;
private Random rng = new Random();
//protected static Logger logger = Logger.getLogger("Thread" + Thread.currentThread().getName());
public RobotInterface(CandidateTest test)
{
this.candidate = test;
//PropertyConfigurator.configure("log4j.properties");
}
public void evaluate (Genome genome)
{
//send to robot and return fitness
genome.setFitness(rng.nextDouble());
//logger.debug("fitness is " + genome.getFitness());
try
{
//logger.debug("Thread sleeping for 4 seconds");
//Thread.sleep(4000);
}
catch(Exception E)
{
}
}
public void run()
{
//logger.debug("entering run of Robot Interface");
//logger.debug("Send Genome via serial and wait for a response");
Genome testSubject = candidate.getCandidate();
evaluate(testSubject);
candidate.finished();
notifyAll();
}
}
The CandidateTest Class
public class CandidateTest
{
private volatile Genome candidate;
private volatile boolean testFinished = false;
public CandidateTest(Genome g)
{
candidate = g;
}
public synchronized Genome getCandidate()
{
return candidate;
}
public synchronized void finished()
{
testFinished = true;
}
public synchronized boolean finishedYet()
{
return testFinished;
}
}
First, you are not starting the robotHandler thread. So your main thread gets to wait() and then no other thread ever comes along to notify it.
Second, you call wait() on whatever class initialFitness belongs to, but you call notifyAll() on RobotInterface. So RobotInterface will notify everyone who is waiting on it (nobody) and your main code will continue to wait. You need to call notifyAll() on the same object on which you called wait().
I suggest
synchronized(candidateTest) {
candidateTest.wait();
}
and
candidateTest.notify();
Never seen where the Thread is started. Try:
Thread robotHandler = new Thread(new RobotInterface( candidateTest)).start();
so your notifyAll() is never called
Nathanial hit the nail on the head but I would suggest using the java.util.concurrent package if you are just getting started with concurrency in Java. Found a nice beginners article on DZone for you: http://java.dzone.com/articles/lazy-developers-introduction

Categories

Resources