`Java 8 in Action` is wrong about the demo it provided? - java

This code is a quoted from Java 8 in Action, which is also in the book 11.4.3.
public Stream<CompletableFuture<String>> findPricesStream(String product) {
return shops.stream()
.map(shop -> CompletableFuture.supplyAsync(() -> shop.getPrice(product), executor))
.map(future -> future.thenApply(Quote::parse))
.map(future -> future.thenCompose(quote -> CompletableFuture.supplyAsync(() -> Discount.applyDiscount(quote), executor)));
}
Along the code, the writer enclose a figure as follows expressing that the applyDiscount() works in the same thread with getPrice(), which I strongly have a doubt: there are two different Async suffix here which means the second call should be in another thread.
I tested it locally with the following code:
private static void testBasic() {
out.println("*****************************************");
out.println("********** TESTING thenCompose **********");
CompletableFuture[] futures = IntStream.rangeClosed(0, LEN).boxed()
.map(i -> CompletableFuture.supplyAsync(() -> runStage1(i), EXECUTOR_SERVICE))
.map(future -> future.thenCompose(i -> CompletableFuture.supplyAsync(() -> runStage2(i), EXECUTOR_SERVICE)))
.toArray(size -> new CompletableFuture[size]);
CompletableFuture.allOf(futures).join();
}
The output further demonstrate my thought, is it correct?
*****************************************
********** TESTING thenCompose **********
Start: stage - 1 - value: 0 - thread name: pool-1-thread-1
Start: stage - 1 - value: 1 - thread name: pool-1-thread-2
Start: stage - 1 - value: 2 - thread name: pool-1-thread-3
Start: stage - 1 - value: 3 - thread name: pool-1-thread-4
Finish: stage - 1 - value: 3 - thread name: pool-1-thread-4 - time cost: 1520
Start: stage - 2 - value: 3 - thread name: pool-1-thread-5
Finish: stage - 1 - value: 0 - thread name: pool-1-thread-1 - time cost: 1736
Start: stage - 2 - value: 0 - thread name: pool-1-thread-6
Finish: stage - 1 - value: 2 - thread name: pool-1-thread-3 - time cost: 1761
Start: stage - 2 - value: 2 - thread name: pool-1-thread-7
Finish: stage - 2 - value: 2 - thread name: pool-1-thread-7 - time cost: 446
Finish: stage - 1 - value: 1 - thread name: pool-1-thread-2 - time cost: 2249
Start: stage - 2 - value: 1 - thread name: pool-1-thread-8
Finish: stage - 2 - value: 3 - thread name: pool-1-thread-5 - time cost: 828
Finish: stage - 2 - value: 0 - thread name: pool-1-thread-6 - time cost: 704
Finish: stage - 2 - value: 1 - thread name: pool-1-thread-8 - time cost: 401
The Java 8 in Action is wrong about this?
Thank you, #Holger. You make it crystal clear to me now about the executing thread for async and non-async methods. Especially after checking its specification further demonstrating your point.
Actions supplied for dependent completions of non-async methods may be performed by the thread that completes the current CompletableFuture, or by any other caller of a completion method.

As a first note, that code is distracting from what’s happening due to the unnecessary splitting into multiple Stream operations.
Further, there is no sense in doing
future.thenCompose(quote ->
CompletableFuture.supplyAsync(() -> Discount.applyDiscount(quote), executor))
instead of
future.thenApplyAsync(quote -> Discount.applyDiscount(quote), executor)
So, a simpler example doing the same would be
public Stream<CompletableFuture<String>> findPricesStream(String product) {
return shops.stream().map(
shop -> CompletableFuture
.supplyAsync(() -> shop.getPrice(product), executor)
.thenApply(Quote::parse)
.thenApplyAsync(quote -> Discount.applyDiscount(quote), executor));
}
However, you are right, there is no guaranty that getPrice and applyDiscount run in the same thread—unless the executor is a single threaded executor.
You may interpret “executor thread” as “one of the executor’s threads”, but even then, there in a dangerously wrong point in the diagram, namely, “new Quote(price)”, which apparently actually means “Quote::parse”. That step does not belong to the right side, as the actual thread evaluating the function passed to thenApply is unspecified. It may be one of the executor’s threads upon completion of the previous stage, but it may also be “your thread” right when calling thenApply, e.g. if the asynchronous operation managed to complete in‑between.
The CompletableFuture offers no way to enforce the use of the first stage’s completing thread for the dependent actions.
Unless you use a simple sequential code instead, of course:
public Stream<CompletableFuture<String>> findPricesStream(String product) {
return shops.stream().map(shop -> CompletableFuture
.supplyAsync(() -> Discount.applyDiscount(Quote.parse(shop.getPrice(product))), executor));
}
Then, the picture of a linear thread on the right hand side will be correct.

Related

Java reactor `suscribe` is sometime blocking, sometime not

I have been playing around for some time with reactor, but I still need to get something.
This piece of code
Flux.range(1, 1000)
.delayElements(Duration.ofNanos(1))
.map(integer -> integer + 1)
.subscribe(System.out::println);
System.out.println("after");
Returns:
after
2
3
4
which is expected as the documentation of subscribe states: this will immediately return control to the calling thread.
Why, then, this piece of code:
Flux.range(1, 1000)
.map(integer -> integer + 1)
.subscribe(System.out::println);
returns
1
2
...
1000
1001
after
I can never figure out when subscribe will block or not, and that's very annoying when writing batches.
If anyone has the answer, that would be amazing
There is no blocking code in your snippet.
In first example you use .delayElements() and it switches the executing to another thread and releases your main thread. So you can see your System.out.println("after"); executing in Main thread immediately, whilst the reactive chain is being executed on parallel-n threads.
Your first example:
18:49:29.195 [main] INFO com.example.demo.FluxTest - AFTER
18:49:29.199 [parallel-1] INFO com.example.demo.FluxTest - v: 2
18:49:29.201 [parallel-2] INFO com.example.demo.FluxTest - v: 3
18:49:29.202 [parallel-3] INFO com.example.demo.FluxTest - v: 4
18:49:29.203 [parallel-4] INFO com.example.demo.FluxTest - v: 5
18:49:29.205 [parallel-5] INFO com.example.demo.FluxTest - v: 6
But your second example does not switch the executing thread, so your reactive chain executes on Main thread. And after it completes it continues to execute your System.out.println("after");
18:51:28.490 [main] INFO com.example.demo.FluxTest - v: 995
18:51:28.490 [main] INFO com.example.demo.FluxTest - v: 996
18:51:28.490 [main] INFO com.example.demo.FluxTest - v: 997
18:51:28.490 [main] INFO com.example.demo.FluxTest - v: 998
18:51:28.490 [main] INFO com.example.demo.FluxTest - v: 999
18:51:28.490 [main] INFO com.example.demo.FluxTest - v: 1000
18:51:28.490 [main] INFO com.example.demo.FluxTest - v: 1001
18:51:28.491 [main] INFO com.example.demo.FluxTest - AFTER
EDIT:
If you want to switch the thread in your second snippet, basically you have two options:
Add subscribeOn(<Scheduler>) in any place of your reactive chain. Then the whole subscription process will happen on a thread from scheduler you provided.
Add publishOn(<Scheduler>), for example, after Flux.range(), then the emitting itself will happen on your calling thread, but the downstream will be executed on a thread from the scheduler you provided

How the use threads in groovy to iterate 0.4 million records

// this query returns 0.45 million records and stored in the list.
List<Employee> empList=result.getQuery(query);
Iterating employee list and setting property and finally calling service method to save employee object.
using sequential process method its taking lot of time because of the volume of records so I want to use threads .I am new to groovy and implemented only simple examples.
How to use threads for below logic using groovy?
for (Employee employee : empList) {
employee.setQuantity(8);
employeeService.save(employee);
}
There are frameworks to do this (gpars comes to mind) and also the java executors framework is a better abstraction than straight up threads, but if we want to keep things really primitive, you can split your list up in batches and run each batch on a separate thread by using something like:
def employeeService = new EmployeeService()
def empList = (1..400000).collect { new Employee() }
def batchSize = 10000
def workerThreads = empList.collate(batchSize).withIndex().collect { List<Employee> batch, int index ->
Thread.start("worker-thread-${index}") {
println "worker ${index} starting"
batch.each { Employee e ->
e.quantity = 8
employeeService.save(e)
}
println "worker ${index} completed"
}
}
println "main thread waiting for workers to finish"
workerThreads*.join()
println "workers finished, exiting..."
class Employee {
int quantity
}
class EmployeeService {
def save(Employee e) {
Thread.sleep(1)
}
}
which, when run, prints:
─➤ groovy solution.groovy
worker 7 starting
worker 11 starting
worker 5 starting
worker 13 starting
worker 17 starting
worker 16 starting
worker 2 starting
worker 18 starting
worker 6 starting
worker 15 starting
worker 12 starting
worker 14 starting
worker 1 starting
worker 4 starting
worker 10 starting
worker 8 starting
worker 9 starting
worker 3 starting
worker 0 starting
worker 20 starting
worker 21 starting
worker 19 starting
worker 22 starting
worker 24 starting
worker 23 starting
worker 25 starting
worker 26 starting
worker 27 starting
worker 28 starting
worker 29 starting
worker 30 starting
worker 31 starting
worker 32 starting
worker 33 starting
worker 34 starting
worker 35 starting
worker 36 starting
worker 37 starting
worker 38 starting
worker 39 starting
main thread waiting for workers to finish
worker 0 completed
worker 16 completed
worker 20 completed
worker 1 completed
worker 3 completed
worker 14 completed
worker 7 completed
worker 12 completed
worker 24 completed
worker 10 completed
worker 6 completed
worker 19 completed
worker 33 completed
worker 27 completed
worker 28 completed
worker 35 completed
worker 17 completed
worker 25 completed
worker 38 completed
worker 4 completed
worker 8 completed
worker 13 completed
worker 9 completed
worker 39 completed
worker 15 completed
worker 36 completed
worker 37 completed
worker 18 completed
worker 30 completed
worker 23 completed
worker 11 completed
worker 32 completed
worker 2 completed
worker 29 completed
worker 26 completed
worker 5 completed
worker 22 completed
worker 31 completed
worker 21 completed
worker 34 completed
workers finished, exiting...
List.collate splits the list of employees into chunks (List<Employee>) of size batchSize. withIndex is just there so that each batch also gets an index (i.e. just a number 0, 1, 2, 3...) for debuggability and tracing.
As we are starting threads, we need to wait for them to complete, the workerThreads*.join() is essentially doing the same thing as:
workerThreds.each { t -> t.join() }
but using a more concise syntax and Thread.join() is a java construct for waiting for a thread to complete.
Use the database, not Java
As commented by cfrick, in real work you would be using SQL to do a mass update of rows. In contrast, looping object by object in Java to update row by row in the database would be inordinately slow compared to a simple UPDATE… in SQL.
But for the sake of exploration, we will ignore this fact, and proceed with your Question.
Trying virtual threads with Project Loom
The correct Answer by Matias Bjarland inspired me to try similar code using the Project Loom technology coming to Java. Project Loom brings virtual threads (fibers) for faster concurrency with simpler coding.
Project Loom is still in the experimental stage, but is seeking feedback from the Java community. Special builds of early-access Java 16 with Project Loom technology built-in are available now for the Linux/Mac/Windows OSes.
My code here uses Java syntax, as I do not know Groovy.
I want to try similar code to the other Answer, creating a simple Employee with a a single member field quantity. And with an EmployeeService offering a save method that simulates writing to a database by merely sleeping a full second.
One major feature of Project Loom is that blocking a thread, and switching to work on another thread, now becomes very cheap. So many of the tricks and techniques used in writing Java code to avoid expensive blocking became unnecessary. So the batching seen in the other Answer should not be needed when using virtual threads. So the code below simply loops all half million Employee objects, and creates a new Runnable object for each one. As each of the new half-million Runnable objects are instantiated, they are submitted to an executor service.
We run this code twice, using either of two kinds of executor services. One is the conventional type using platform/kernel threads used for many years in Java before Project Loom, specifically, the executor service backed by a fixed thread pool. The other kind is the new executor service offered in Project Loom for virtual threads.
Executors.newFixedThreadPool( int countThreads )
Executors.newVirtualThreadExecutor()
Code
package work.basil.example;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class HalfMillion
{
public static void main ( String[] args )
{
HalfMillion app = new HalfMillion();
app.demo();
}
private void demo ( )
{
System.out.println( "java.runtime.version " + System.getProperty( "java.runtime.version" ) );
System.out.println( "INFO - `demo` method starting. " + Instant.now() );
// Populate data.
List < Employee > employees = IntStream.rangeClosed( 1 , 500_000 ).mapToObj( i -> new Employee() ).collect( Collectors.toList() );
// Submit task (updating field in each object) to an executor service.
long start = System.nanoTime();
EmployeeService employeeService = new EmployeeService();
try (
//ExecutorService executorService = Executors.newFixedThreadPool( 5 ) ; // 5 of 6 real cores, no hyper-threading.
ExecutorService executorService = Executors.newVirtualThreadExecutor() ;
)
{
employees
.stream()
.forEach(
employee -> {
executorService.submit(
new Runnable()
{
#Override
public void run ( )
{
employee.quantity = 8;
employeeService.save( employee );
}
}
);
}
);
}
// With Project Loom, the code blocks here until all submitted tasks have finished.
Duration duration = Duration.ofNanos( System.nanoTime() - start );
// Report.
System.out.println( "INFO - Done running demo for " + employees.size() + " employees taking " + duration + " to finish at " + Instant.now() );
}
class Employee
{
int quantity;
#Override
public String toString ( )
{
return "Employee{ " +
"quantity=" + quantity +
" }";
}
}
class EmployeeService
{
public void save ( Employee employee )
{
//System.out.println( "TRACE - An `EmployeeService` is doing `save` on an employee." );
try {Thread.sleep( Duration.ofSeconds( 1 ) );} catch ( InterruptedException e ) {e.printStackTrace();}
}
}
}
Results
I ran that code on a Mac mini (2018) with 3 GHz Intel Core i5 processor having 6 real cores and no hyper-threading, with 32 GB 2667 MHz DDR4 memory, and running macOS Mojave 10.14.6.
Using the new virtual threads of Project Loom
Using Executors.newVirtualThreadExecutor() takes under 5 seconds.
java.runtime.version 16-loom+9-316
INFO - `demo` method starting. 2020-12-21T09:20:36.273351Z
INFO - Done running demo for 500000 employees taking PT4.517136095S to finish at 2020-12-21T09:20:40.885315Z
If I enabled the println line within the save method, it took 15 seconds.
Using a fixed pool of 5 conventional platform/kernel threads
Using Executors.newFixedThreadPool( 5 ) takes … well, *much longer. Over a day instead of seconds: 27 hours.
java.runtime.version 16-loom+9-316
INFO - `demo` method starting. 2020-12-21T09:32:07.173561Z
INFO - Done running demo for 500000 employees taking PT27H58M18.930703698S to finish at 2020-12-22T13:30:28.813345Z
Conclusion
Well I’m not sure I can draw a conclusion here.
The results for the conventional thread pool make sense. Remember that each Java thread maps to a kernel thread in the host OS. If we are sleeping one second per employee object, as we saturate 5 cores there will mostly be 5 threads sleeping most of the time. This means the total duration should be at least a hundred thousand seconds.
The results for virtual threads on Project Loom are not believable. The command to sleep the current thread seems to ignored when using virtual threads. But I am not certain; perhaps my five physical cores on this Mac were able to be sleeping simultaneously about a hundred thousand threads each?
Please post criticisms if you find fault with my code or approach. I am not an expert on threading and concurrency.

Execute a task on completion of ParallelFlux without executing sequential (introduce a side affect)

I have a ParallelFlux, want to execute a side effect action when all the components in all the rails are consumed. I was trying to use .then().
But unable to understand how to use it.
Can anybody share its usage or a way to execute a side affect after all the elements go through OnError,OnComplete across rails.
Indicative code :
RunTransformation provides a Parallel Flux in transformation,
OnCompletion mark record as completed in a separate registry.
RunAction does some action for each transformed record (independent of the other).
RunError handles error.
Here I want to run RunCompletion only on final completion, but have to do sequential though consumers can be done in parallel.
Mono.just(record)
.flatMap(RunTransformation::tranformParallel) //gives back ParallelFlux running on Schedulers.random()
.sequential()
.doOnTerminate(OnCompletion::markRecordProcessed)
.subscribe(
RunAction::execute,
RunError::handleError);
By using .then() as follows.
Mono.just(record)
.flatMap(RunTransformation::tranformParallel) //gives back ParallelFlux running on Schedulers.random()
.doOnNext(RunAction::execute)
.doOnError(RunError::handleError)
.then()
.doOnTerminate(() -> {System.out.println("all rails completed");})
.subscribe();
taken from the documentation
If, once you process your sequence in parallel, you want to revert back to a “normal” Flux and apply the rest of the operator chain in a sequential manner, you can use the sequential() method on ParallelFlux.
i think doOnComplete is what you are looking for.
Flux.range(1, 10)
.parallel(3)
.runOn(Schedulers.parallel())
.doOnNext(i -> System.out.println(Thread.currentThread().getName() + " -> " + i))
.sequential()
.doOnComplete(() -> System.out.println("All parallel work is done"))
.subscribe()
This produces the output:
parallel-1 -> 1
parallel-2 -> 2
parallel-3 -> 3
parallel-2 -> 5
parallel-3 -> 6
parallel-1 -> 4
parallel-1 -> 7
parallel-2 -> 8
parallel-3 -> 9
parallel-1 -> 10
All parallel work is done
Reactor documentation on parallel flux

Why doesn't my RxJava Flowable respect backpressure when using observeOn?

I am trying to create a Flowable which emits events respecting backpressure to avoid memory issues, while running each stage of transformation in parallel for efficiency. I have created a simple test program to reason about the behavior of the different steps of my program and when events are being emitted vs. waiting on different stages.
My program is as follows:
public static void main(String[] args) throws ExecutionException, InterruptedException {
Stream<Integer> ints = IntStream.range(0, 1000).boxed().collect(Collectors.toList())
.stream().map(i -> {
System.out.println("emitting:" + i);
return i;
});
Flowable<Integer> flowable = Flowable.fromIterable(() -> ints.iterator());
System.out.println(String.format("Buffer size: %d", flowable.bufferSize()));
Long count = flowable.onBackpressureBuffer(10)
.buffer(10)
.flatMap(buf -> {
System.out.println("Sleeping 500 for batch");
Thread.sleep(500);
System.out.println("Got batch of events");
return Flowable.fromIterable(buf);
}, 1)
.map(x -> x + 1)
.doOnNext(i -> {
System.out.println(String.format("Sleeping : %d", i));
Thread.sleep(100);
System.out.println(i);
})
.count()
.blockingGet();
System.out.println("count: " + count);
}
When I run this, I get output that respects backpressure as expected, where a batch of events is emmited up to the size in buffer, then they are flatmapped, and finally some action is taken where they are printed one-by-one:
Buffer size: 128
emitting:0
emitting:1
emitting:2
emitting:3
emitting:4
emitting:5
emitting:6
emitting:7
emitting:8
emitting:9
Sleeping 500 for batch
Got batch of events
Sleeping : 1
1
Sleeping : 2
2
Sleeping : 3
3
Sleeping : 4
4
Sleeping : 5
5
Sleeping : 6
6
Sleeping : 7
7
Sleeping : 8
8
Sleeping : 9
9
Sleeping : 10
10
emitting:10
emitting:11
emitting:12
emitting:13
emitting:14
emitting:15
emitting:16
emitting:17
emitting:18
emitting:19
Sleeping 500 for batch
Got batch of events
Sleeping : 11
11
Sleeping : 12
12
Sleeping : 13
However if I attempt to parallelize the different stages of operation here by adding some calls to .observeOn(Schedulers.computation()) then it seems like my program no longer respects backpressure. My code now looks like:
public static void main(String[] args) throws ExecutionException, InterruptedException {
Stream<Integer> ints = IntStream.range(0, 1000).boxed().collect(Collectors.toList())
.stream().map(i -> {
System.out.println("emitting:" + i);
return i;
});
Flowable<Integer> flowable = Flowable.fromIterable(() -> ints.iterator());
System.out.println(String.format("Buffer size: %d", flowable.bufferSize()));
Long count = flowable.onBackpressureBuffer(10)
.buffer(10)
.observeOn(Schedulers.computation())
.flatMap(buf -> {
System.out.println("Sleeping 500 for batch");
Thread.sleep(500);
System.out.println("Got batch of events");
return Flowable.fromIterable(buf);
}, 1)
.map(x -> x + 1)
.observeOn(Schedulers.computation())
.doOnNext(i -> {
System.out.println(String.format("Sleeping : %d", i));
Thread.sleep(100);
System.out.println(i);
})
.observeOn(Schedulers.computation())
.count()
.blockingGet();
System.out.println("count: " + count);
}
And my output is the following, where all of my events are emitted upfront instead of respecting the backpressure and buffers specified by the various stages of execution:
Buffer size: 128
emitting:0
emitting:1
emitting:2
emitting:3
emitting:4
emitting:5
emitting:6
emitting:7
emitting:8
emitting:9
emitting:10
Sleeping 500 for batch
emitting:11
emitting:12
... everything else is emitted here ...
emitting:998
emitting:999
Got batch of events
Sleeping 500 for batch
Sleeping : 1
1
Sleeping : 2
2
Sleeping : 3
3
Sleeping : 4
4
Sleeping : 5
Got batch of events
Sleeping 500 for batch
5
Sleeping : 6
6
Sleeping : 7
7
Sleeping : 8
8
Sleeping : 9
9
Sleeping : 10
Got batch of events
Sleeping 500 for batch
10
Sleeping : 11
11
Sleeping : 12
12
Sleeping : 13
13
Sleeping : 14
14
Sleeping : 15
Got batch of events
Sleeping 500 for batch
15
Sleeping : 16
16
Sleeping : 17
17
Sleeping : 18
18
Sleeping : 19
19
Sleeping : 20
Got batch of events
Sleeping 500 for batch
20
Sleeping : 21
21
Sleeping : 22
22
Sleeping : 23
23
Sleeping : 24
24
Sleeping : 25
Got batch of events
Sleeping 500 for batch
25
Pretend my stages of batching are calling out to external services, but that I want them to run in parallel because of latency. I also want to have control of the number of items in memory at a given time because the number of items emitted initially could be highly variable, and the stages operating on batches run much slower than the initial emission of events.
How can I have my Flowable respect backpressure across a Scheduler? Why does it seem to only disrespect backpressure when I sprinkle in calls to observeOn?
How can I have my Flowable respect backpressure across a Scheduler
Actually, applying onBackpressureBuffer makes the source above it disconnect from any backpressure applied by downstream as it is an unbounded-in operator. You don't need it because Flowable.fromIterable (and by the way, RxJava has a range operator) supports and honors backpressure.
Why does it seem to only disrespect backpressure when I sprinkle in calls to observeOn?
In the first example, there is a natural backpressure happening called call-stack blocking. RxJava is synchronous by default and most operators don't introduce asynchrony, just like none do in the first example.
observeOn introduces an asynchronous boundary thus in theory, stages can run in parallel with each other. It has a default 128 element prefetch buffer which can be adjusted via one of its overloads. In your case, however, buffer(10) will actually amplify the prefetch amount to 1280 which may still lead to the complete consumption of your 1000 element long source in one go.

RxJava scheduler always works in the same thread with sleep

I have tried to run each computation on different thread, but whatever Scheduler i used it running always on single thread.
PublishProcessor processor = PublishProcessor.create();
processor
.doOnNext(i ->System.out.println(i.toString()+" emitted on "+Thread.currentThread().getId()))
.observeOn(Schedulers.newThread()).subscribe(i -> {
System.out.println(i.toString()+" received on "+Thread.currentThread().getId());
Thread.currentThread().sleep(5000);
});
processor.onNext(2);
processor.onNext(3);
processor.onNext(4);
processor.onNext(5);
processor.onNext(6);
while (true) {}
The output would be:
2 emitted on 1
3 emitted on 1
4 emitted on 1
5 emitted on 1
6 emitted on 1
2 received on 13
3 received on 13
4 received on 13
5 received on 13
6 received on 13
Thread 13 processes the next value only after sleeping, but i want to have few separate sleeping threads in that case.
Can someone explain what I'm doing wrong, please?
.observeOn(...) makes effect by changing to the item flow to another thread but it's always the same thread.
If you want to create a new thread for every item you can do
processor
.doOnNext(i ->System.out.println(i.toString()+" emitted on "+Thread.currentThread().getId()))
.flatMap(item -> Observable.just(item)
.subscribeOn(Schedulers.newThread())) // make every item change to a new thread
.subscribe(i -> {
System.out.println(i.toString()+" received on "+Thread.currentThread().getId());
Thread.currentThread().sleep(5000);
});

Categories