Using ExecutorService, Callable API to run Multi-threaded JUNIT tests

Overview

  • In this tutorial we use ExecutorService, Callable API  and JUNIT to test your program for thread safty
  • Callable vs Runable
    • Runnable interface is older than Callable, there from JDK 1.0, while Callable is added on Java 5.0.
    • Runnable interface has run() method to define task while Callable interface uses call() method for task definition.
    • run() method does not return any value, it’s return type is void while call method returns value. Callable interface is a generic parameterized interface and Type of value is provided, when instance of Callable implementation is created.
    • Another difference on run and call method is that run method can not throw checked exception, while call method can throw checked exception in Java.

Test case details

  • Assume on of your developer has created the following ThreadCounter class.
  • This class works fine in single threaded Apps but fails if more than 1 Thread is using this Counter

JAVA code : JUnitTest/src/main/java/com/hhu/junittest/ThreadCounter.java

package com.hhu.junittest;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;

@ManagedBean
@SessionScoped
public class ThreadCounter
    {
    private  int count = 0;
    public final VolatileInt vi = new VolatileInt();    
    
    public ThreadCounter()
      {
      }    
    public ThreadCounter( int count)
      {
        this.count = count;
      }        
    public void setCounter( int count)
      {
        this.count = count;
      }
    public static void main(String[] args)
      {
        System.out.println("Hallo from ThreadCounter Class");
          // TODO code application logic here
        // final VolatileInt vi = new VolatileInt();
        ThreadCounter tc = new ThreadCounter();
        tc.setCounter(100*1000*1000);
        for (int j = 0; j < tc.count; j++)
            tc.testIncrement();
        tc. counterStatus();
      }
    
    public void testIncrement()
      {
        vi.num++;
      }
    
    public int getCounter()
      {
        return vi.num;
      }
    static class VolatileInt 
      {
        volatile int num = 0;
      }
    
    public void counterStatus()
      {
         System.out.printf("Total %,d but was %,d", count, getCounter() );
          if ( count == vi.num   )
                System.out.printf(" - Test : OK \n");
            else
               System.out.printf(" - Test : FAILED \n");
      }   
    }

The single Thread test works fine and returns : 
  Hallo from ThreadCounter Class
  Total 100,000,000 but was 100,000,000 - Test : OK 

Test Code for testing thread-safty : JUnitTest/src/test/java/ThreadTest.java

JAVA Code :
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import com.hhu.junittest.ThreadCounter;

import org.junit.Assert;
import org.junit.Test;
 
public class ThreadTest
  {
    
    /*
     static class VolatileInt 
      {
        volatile int num = 0;
      }
    */
    private void test(final int threadCount, boolean atomicTest ) throws InterruptedException, ExecutionException 
      {
        final boolean atomicTestf = atomicTest;        
       // final VolatileInt vi = new VolatileInt();          // This test fails for threadCount > 1
        final AtomicInteger num = new AtomicInteger();     // This test works for any threadCount value
        
        final int count = 100 *1000*1000;
        
        final ThreadCounter tc = new ThreadCounter(100*1000*1000);
            // Create an Anonymous Innner Class with a Callable Object
            // Use Callable  object so we get some results back from our thread test
        Callable<String> task = new Callable<String>() 
          {
            @Override
            public String call() 
              {
                for (int j = 0; j < count; j += threadCount)
                  {
                    if (atomicTestf)
                        num.incrementAndGet();
                    else 
                      //  vi.num++;
                      tc.testIncrement();
                  }       
                return "atomicTest: " + atomicTestf + " - Leaving Thread:  " + Thread.currentThread().getName() +
                   " - nThreads: "  + threadCount; 
              }                
          };
           // As we want to run the same Callable Objects on all threads use:  Collections.nCopies   
           // Collections.nCopies returns an immutable list consisting of n copies of the specified object. 
           // Note the newly allocated data object contains only a single reference to the data object.
        List<Callable<String>> tasks = Collections.nCopies(threadCount, task);
           //Get ExecutorService from Executors utility class, thread pool size is threadCount 
        ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
           // The invokeAll() method invokes all of the Callable objects you pass to it in the collection passed as parameter. 
           // The invokeAll() returns a list of Future objects via which you can obtain the results of the executions of each Callable.
        List<Future<String>> futures = executorService.invokeAll(tasks);
           // create new and empty String ArrayList
        List<String> resultList = new ArrayList<String>(futures.size());
        // Check for exceptions - our Futures are returning  String Objects
        for (Future<String> future : futures) 
          {
            // Throws an exception if an exception was thrown by the task.
            resultList.add(future.get());
          }
        // Note the future.get() blocks until we get some return value
        // At this stage all tasks are finished - either with an Exception or be a regular method return
        // Validate the final Thread Counter status        
        if (atomicTest)
          {
            System.out.printf("With %,d threads should total %,d but was %,d",
                threadCount , count, num.intValue() ); 
             if ( count == num.intValue()   )
                System.out.printf(" - Test : OK \n");
            else
               System.out.printf(" - Test : FAILED \n");
            Assert.assertEquals(count,num.intValue());
          }
        else
          {
            System.out.printf("With %,d threads should total %,d but was %,d",
                threadCount , count,  tc.getCounter()  ); 
            if ( count == tc.getCounter()  )
                System.out.printf(" - Test : OK \n");
            else
               System.out.printf(" - Test : FAILED \n"); 
            Assert.assertEquals(count,  tc.getCounter());
          }
           // Display the results 
        for ( int i = 0; i<resultList.size(); i++)
            System.out.println(resultList.get(i) );
      }
 
    @Test
    public void test01() throws InterruptedException, ExecutionException 
      {
        test(1,false);
      }
 
    @Test
    public void test02() throws InterruptedException, ExecutionException 
      {
        test(2,false);
      } 
 
    @Test
    public void test04() throws InterruptedException, ExecutionException 
      {
        test(4,false);
      }
 
    @Test
    public void test08() throws InterruptedException, ExecutionException 
      {
        test(8,false);
      }
 
    @Test
    public void test16() throws InterruptedException, ExecutionException 
      {
        test(8,true);    
   } 
 }

Test results

[oracle@wls1 JUnitTest]$ mvn test
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running ThreadTest
With 1 threads should total 100,000,000 but was 100,000,000 - Test : OK 
atomicTest: false - Leaving Thread:  pool-1-thread-1 - nThreads: 1
With 2 threads should total 100,000,000 but was 74,553,473 - Test : FAILED 
With 4 threads should total 100,000,000 but was 40,718,556 - Test : FAILED 
With 8 threads should total 100,000,000 but was 23,405,246 - Test : FAILED 
With 8 threads should total 100,000,000 but was 100,000,000 - Test : OK 
atomicTest: true - Leaving Thread:  pool-5-thread-1 - nThreads: 8
atomicTest: true - Leaving Thread:  pool-5-thread-2 - nThreads: 8
atomicTest: true - Leaving Thread:  pool-5-thread-3 - nThreads: 8
atomicTest: true - Leaving Thread:  pool-5-thread-4 - nThreads: 8
atomicTest: true - Leaving Thread:  pool-5-thread-5 - nThreads: 8
atomicTest: true - Leaving Thread:  pool-5-thread-6 - nThreads: 8
atomicTest: true - Leaving Thread:  pool-5-thread-7 - nThreads: 8
atomicTest: true - Leaving Thread:  pool-5-thread-8 - nThreads: 8
Tests run: 5, Failures: 3, Errors: 0, Skipped: 0, Time elapsed: 2.253 sec <<< FAILURE!

Results :
Failed tests:   
  test02(ThreadTest): expected:<100000000> but was:<74553473>
  test04(ThreadTest): expected:<100000000> but was:<40718556>
  test08(ThreadTest): expected:<100000000> but was:<23405246>
Tests run: 5, Failures: 3, Errors: 0, Skipped: 0

Some comments on above test results

  • test01 works as we use only a single Thread
  • test02, test04, test08 fails if using 2,4 or 8 Threads and if using volatile int as ThreadCounter
  • test08 works even for 8 Threads if using AtomicInteger() instead of volatile int as ThreadCounter
  • test08 with AtomicInteger() returns a String object for each Thread by using our Future objects

 

Reference

Leave a Reply

Your email address will not be published. Required fields are marked *