1

I have a multithreaded Java application that uses several threads that are CPU intensive to gather information. Once every few minutes, a result is found that requires handling by another thread of the program. The found result is added to a list and the other relevant thread is notified (using Lock and Condition), after which it handles the found information. I need the time delay for passing this information from thread to thread to be as small as possible. When measuring the time between wake-up and notify using System.currentTimeMillis(), the delay is usually smaller than 5 ms, and most often less than or equal to 1 ms. Sometimes, the delay is larger (10-20ms). Since milliseconds are macro-units when it comes to computers, I would think that a delay that is reliably smaller than 1ms should be possible, and it would benefit my application.

Do you have any idea what the cause of the larger delays can be, or how I can find out where to look? Could it be Garbage Collection? Or are several milliseconds of variation for thread wakeup considered normal?

I am using Java version 1.8.0 on a Linux Ubuntu virtual private server.

An example of the design of the program is attached. Running this does not simulate the delays as observed by my production program correctly. The 'actual' program uses a lot of memory, CPU and only transmits a bit of info once every few minutes. I tried but failed in simulating this simply.

Thank you.

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.Condition;
import java.util.List;
import java.util.ArrayList;
import java.util.Random;

public class Example {
    public static void main(String[] args) {
        startInfoThreads();
        startWatcherThread();
    }

    private static Lock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();
    private static List<Long> infoList = new ArrayList<>();

    private static void startWatcherThread () {
        Thread t = new Thread () {
            @Override
            public void run () {
                while (true) {
                    // Waiting for results...
                    try {
                        lock.lock();
                        while (infoList.size() == 0) {
                            try {
                                condition.await();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        long delta = System.currentTimeMillis() - infoList.remove(0);
                        if (delta > 0)
                            System.out.println("Time for waking up: " + delta);
                    } finally {
                        lock.unlock();
                    }
                    // Do something with info
                }
            }
        };
        t.start();
    }

    private static void startInfoThreads () {
        for (int i = 0; i < 14; i++) {
            Thread t = new Thread() {
                @Override
                public void run() {
                    Random r = new Random();
                    while (true) {
                        // Gather info, 'hits' about once every few minutes!
                        boolean infoRandomlyFound = r.nextInt(100) >= 99;
                        if (infoRandomlyFound) {
                            try {
                                lock.lock();
                                infoList.add(System.currentTimeMillis());
                                condition.signal();
                            } finally {
                                lock.unlock();
                            }
                        }
                    }
                }
            };
            t.start();
        }
    }
}
Jeroen
  • 61
  • 6
  • 1
    See this incredibly detailed article titled [The slow currentTimeMillis()](http://pzemtsov.github.io/2017/07/23/the-slow-currenttimemillis.html). I think the author wrote it just for you! As a separate matter, you might update your question to specify your Java version and O/S. – skomisa Aug 15 '18 at 17:37
  • How many cores does your setup have and how many threads are created/executing? – Impurity Aug 15 '18 at 18:34
  • 8 core setup. About 11 threads are gathering information 'violently' and another thread is idly waiting for information to arrive. Upgrading to a 16 core setup might be suitable, but is quite expensive for my current hoster. – Jeroen Aug 15 '18 at 19:01
  • There is way too much code to make a coherent presentation that highlights the issue, so I don't think showing some would help. – Jeroen Aug 15 '18 at 19:01
  • 2
    @Jeroen Re _"there is way too much code"_ => Is it possible to create an MCVE? See [How to create a Minimal, Complete, and Verifiable example](https://stackoverflow.com/help/mcve) – skomisa Aug 15 '18 at 19:11
  • Get a profiler and check where the code spends its time. – Robert Aug 15 '18 at 19:57
  • How is the CPU use under load? 100% or less? – rustyx Aug 15 '18 at 20:28
  • Won't affect your speed significantly but using locks for signaling the arrival of something in a `List` seems strange. Why aren't you using a `BlockingQueue`? – OldCurmudgeon Aug 16 '18 at 06:58

2 Answers2

0

System.currentTimeMillis() can be affected by clock drift and usually have a granularity of ~10ms.

To measure elapsed time you should always use System.nanoTime() as it guarantees accuracy.

Karol Dowbecki
  • 43,645
  • 9
  • 78
  • 111
  • Can you elaborate on the concept of clock drift? Also, I have read elsewhere that System.nanoTime() is not suitable for measuring between threads. – Jeroen Aug 15 '18 at 17:30
  • Thanks for the link. Check out this answer in the thread you linked to, though: https://stackoverflow.com/a/9314616/6637018 – Jeroen Aug 15 '18 at 17:35
  • @KarolDowbecki "[...] no guarantees are made except that the resolution is at least as good as that of currentTimeMillis()" from the docs. We may be using the word "guarantee" differently. – Dave Newton Aug 15 '18 at 17:35
  • If you are running on a virtual private server, your app's behavior will probably be affected by whatever else is going on in the other VMs that are hosted on the same physical hardware as your VM's - so variations on the order of msecs are probably not unreasonable... – moilejter Aug 15 '18 at 19:43
  • @Jeroen Note this issue: _"The large jumps in the Linux timings turn out to be due to discrepancies between the TSC counters held on different CPUs. The JVM will periodically suspend the running Java thread and migrate it to running on a different core. This can cause the differences between the CPU counters to become visible to application code. **This means that nanoTime() can become basically untrustworthy over long periods of time.** It’s useful for measuring short durations of time, but over longer (macro-scopic) timeframes, it should be rebaselined against currentTimeMillis()."_ – skomisa Aug 25 '18 at 23:35
  • @Jeroen The quote in my previous comment was taken from Chapter 6 (_"Understanding Performance Tuning"_) of _"The Well-Grounded Java Developer"_ by Benjamin Evans and Martijn Verburg from Manning Publications. Be careful when using **System.nanoTime()** on Linux. And note that this is for a single thread. – skomisa Aug 25 '18 at 23:40
-1

It probably will not speed up your process but using a BlockingQueue would certainly make the code clearer.

Also note the Thread.sleep for when there is no info.

final BlockingQueue<Long> queue = new ArrayBlockingQueue<>(10);

private void startWatcherThread() {
    Thread t = new Thread() {
        @Override
        public void run() {
            while (true) {
                // Waiting for results...
                try {
                    Long polled = queue.poll(1, TimeUnit.SECONDS);
                    // Do something with info
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    };
    t.start();
}

private void startInfoThreads() {
    for (int i = 0; i < 14; i++) {
        Thread t = new Thread() {
            @Override
            public void run() {
                Random r = new Random();
                while (true) {
                    // Gather info, 'hits' about once every few minutes!
                    boolean infoRandomlyFound = r.nextInt(100) >= 99;
                    if (infoRandomlyFound) {
                        queue.put(System.currentTimeMillis());
                    } else {
                        try {
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        };
        t.start();
    }
}

private void test() {
    startInfoThreads();
    startWatcherThread();

}
OldCurmudgeon
  • 64,482
  • 16
  • 119
  • 213