📜 ⬆️ ⬇️

Migrate java application to Fork / Join or what you need to remember

With the release of the seventh version of the JDK, we, happy developers on Java , have become available from the box framework Fork / Join , which has already been written on Habré here . The framework in terms of API is very similar to the already familiar ExecutorService s, but it gives a very tangible performance gain and a real "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 a guarantee that one task from the beginning to the end is performed by one thread.
In Fork / Join, work with threads has undergone dramatic changes. Tasks ( ForkJoinTask 's) have a different semantics than streams: one thread can perform several tasks intersecting 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 began to perform a new task . This is faster: there is no need to start another stream and we avoid context switching. After the end of the task, the original task continued its execution.

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

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

Moral : do not use ThreadLocal variables in this case, or implement your analog ThreadLocal , supporting ForkJoinTask .

Locks


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

For example, we have a ThreadPoolExecutor , with a top-limited pool size. Let's say this one, for simplicity. We run 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 streams. If the pool is larger, then we can consider the case when the other threads are waiting for each other and are in deadlock. Or even simpler, one task gave birth to the second, that third, and so on, and everyone is waiting for the results of the execution of the running tasks.

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

To provide the necessary level of parallelism, ForkJoinPool 'e provides a controlled locking mechanism. Using the ForkJoinPool.ManagedBlocker class , we can tell ForkJoinPool 'y that the thread can block while waiting for the lock and ForkJoinPool will create an additional thread to provide a given 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 the lock as follows in the code:
 ReentrantLock lock = new ReentrantLock(); //Somewhere in thread try{ ForkJoinPool.managedBlock(new ManagedLocker(lock)); //Guarded code goes here }finally{ lock.unlock(); } 


Everything.

And may the force be with you!

Source: https://habr.com/ru/post/134183/


All Articles