Migrating a java application to Fork / Join or what you need to remember

With the release of the seventh version of the JDK, we, happy Java developers , became available from the Fork / Join framework , which was already written about in the hub here . The framework in terms of the API is very similar to the already familiar ExecutorService s, but it gives a very tangible performance boost and the actual "lightness" of threads.

Here, I would like to consider what you should pay attention to when switching to Fork / Join .


Threadlocal variables


With ExecutorService , we had the guarantee that one task from the beginning to the end is performed by a single thread.
In Fork / Join , threading has undergone major changes. Tasks ( ForkJoinTask ’s) already have different semantics than threads: one thread can perform several tasks that overlap in time.

For example, when calling task.invoke () , a scenario is quite possible when the original task was executed by one thread, then the same thread started to execute a new task task . This is faster: you do not need to start another thread and we avoid context switching. After the end of the task, the original task continued its execution.

Therefore, the approach to using local flow variables should be reviewed.
ThreadLocal can be used in several cases:
  1. For storing any non-thread-safe utility classes. For example, SimpleDateFormat . The creation of which is very expensive, and the use of multiple threads is fraught with incorrect operation and exceptions.
  2. To store any thread execution context. For example, a current transaction, a connection to a database, etc. Or data that simply decided to transfer not through the method signature or setter, but through a context local to the stream. For example, an ActionContext in Struts2 .

If in the first case nothing terrible happens when switching to Fork / Join , then in the second, the local variables of one task will become available to another.

Moral : do not use ThreadLocal variables in this case, or implement your ThreadLocal analogue that supports ForkJoinTask .

Locks


In general, the framework does not impose restrictions on the use of other means of locking and synchronization. Moreover, it helps to avoid thread freezing situations while waiting for other threads ( thread starvation ).

For example, we have ThreadPoolExecutor , with the pool size limited from above. Let's say this is one, for simplicity. We start one thread, which in turn adds another thread to the queue and waits for it to complete. In this case, we will never wait for both flows. If the pool is larger, then we can consider the case when other threads are waiting for each other and are in deadlock. Or even simpler, one task gave rise to a second, that third, and so on, and everyone is waiting for the results of the running tasks.

Part of the problem is solved by join ()essentially returns the thread to the pool.

To ensure the required level of concurrency, ForkJoinPool provides a controlled locking mechanism. Using the ForkJoinPool.ManagedBlocker class , we can tell ForkJoinPool 'y that the thread can block waiting for the lock and ForkJoinPool will create an additional thread to provide the specified level of parallelism.

Suppose we want to use ReentrantLock in our code. We need to implement the ForkJoinPool.ManagedBlocker interface as follows (taken from javadocs):

class ManagedLocker implements ManagedBlocker {
   final ReentrantLock lock;
   boolean hasLock = false;
   ManagedLocker(ReentrantLock lock) { 
      this.lock = lock; 
   }
   public boolean block() {
     if (!hasLock)
       lock.lock();
     return true;
   }
   public boolean isReleasable() {
     return hasLock || (hasLock = lock.tryLock());
   }
 }

and use lock as follows in code:
ReentrantLock lock = new ReentrantLock();
//Somewhere in thread
try{
   ForkJoinPool.managedBlock(new ManagedLocker(lock));
   //Guarded code goes here
}finally{
   lock.unlock();
}


All.

And may the force come with you!

Also popular now: