Fun with Generics and Java Concurrency

by Michael

I had previously worked with C#’s asynchronous framework and wanted to play around with Java’s concurrency framework to see how similar they are. This is just a small program that is supposed to be fun and nothing to do with coding effectively. I also threw in some overkill code to sum the items of an array – a reduce operation using generics.

The program blurs an image by calculating the average colour values of neighbouring pixels. One can apply a colour bias to “prefer” certain colours or colour combinations. A blur operation is inherently parallel, i.e., we have no dependencies between the results of the blur – all work is performed using the original image as the input. This makes it quite easy to split the work across different threads. Optimally, one chooses as many threads as one has cores.

Here is the Java code (again, this is just for fun, not best practice)

import <...>

public class SandboxOne {

    static private int numActiveThreads;
    static private long tic;
    static private ExecutorService threadPool;

    public interface AsyncCallback<T> {
        void call(T data);
    }

    public interface Reducer<S,T> {
        T reduce(S current, T result);
    }

    public static class Algorithms {

        public static <S,T> T reduce(Collection<S> items, Reducer<S,T> reducer, T initial) {

            for (S item : items) {
                initial = reducer.reduce(item, initial);
            }
            return initial;
        }
    }

    private static class BlurImage implements Callable<Object> {

        private final int start;
        private final int step;
        private final AsyncCallback callback;
        private final BufferedImage original;
        private final BufferedImage processed;

        private final Reducer<Integer, Integer> sumIntegers = new Reducer<Integer, Integer>() {
            public Integer reduce(Integer current, Integer result) {
                return result + current;
            }
        };

        public BlurImage(BufferedImage original, BufferedImage processed, int start, int step, AsyncCallback<BufferedImage> callback) {
            this.original = original;
            this.processed = processed;
            this.start = start;
            this.step = step;
            this.callback = callback;
        }

        public Object call() {

            int imageWidth  = original.getWidth();
            int imageHeight = original.getHeight();

            System.out.println("started");

            int red;
            int green;
            int blue;
            int colors[] = new int[8];
            Integer colorBias[] = {1, 5, 1};

            int sumColorBias = Algorithms.reduce(Arrays.asList(colorBias), sumIntegers, 0) / 3;

            int end = Math.min(start + 1 + step, imageWidth - 2);

            for (int x = start + 1; x < end; x++) {
                for (int y = 1; y < imageHeight - 2; y++) {

                    red   = 0;
                    green = 0;
                    blue  = 0;

                    colors[0] = original.getRGB(x - 1, y - 1);
                    colors[1] = original.getRGB(x    , y - 1);
                    colors[2] = original.getRGB(x + 1, y - 1);
                    colors[3] = original.getRGB(x - 1, y);
                    colors[4] = original.getRGB(x + 1, y);
                    colors[5] = original.getRGB(x - 1, y + 1);
                    colors[6] = original.getRGB(x    , y + 1);
                    colors[7] = original.getRGB(x + 1, y + 1);

                    for (int color : colors) {
                        red     += colorBias[0] * ((color & 0x00ff0000) >> 16);
                        green   += colorBias[1] * ((color & 0x0000ff00) >> 8);
                        blue    += colorBias[2] *  (color & 0x000000ff);
                    }

                    int newColor = (Math.min(255, (red   / (8 * sumColorBias))) << 16)
                                 + (Math.min(255, (green / (8 * sumColorBias))) << 8)
                                 +  Math.min(255, (blue  / (8 * sumColorBias)));

                    processed.setRGB(x, y, newColor);
                }
            }

            callback.call(processed);
            return null;
        }
    }

    private static final AsyncCallback render = new AsyncCallback<BufferedImage>() {

        public void call(BufferedImage data) {
            numActiveThreads--;

            if (numActiveThreads != 0) return;

            long toc = System.currentTimeMillis() - tic;

            JFrame frame = new JFrame();
            frame.getContentPane().add(new JLabel(new ImageIcon(data)), BorderLayout.CENTER);
            frame.pack();
            frame.setVisible(true);
            frame.setTitle("Time taken: " + toc + "ms");
        }
    };

    private static int calcStepSize(int total, int numThreads, int currentThread) {

        int step = total / numThreads;
        int rem  = total % numThreads;

        if (currentThread >= rem) return step;
        return step + 1;
    }

    private static ExecutorService getThreadPool() {
        if (threadPool == null) {
            threadPool = Executors.newFixedThreadPool(getNumProcessors());
        }
        return threadPool;
    }

    private static int getNumProcessors() {
        return Runtime.getRuntime().availableProcessors();
    }

    public static void main(String[] args) {

        BufferedImage original  = null;
        BufferedImage processed = null;
        URL url;

        try {
            url = new URL("some_image_url.jpg");
            original  = ImageIO.read(url);
            processed = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_RGB);
        } catch (MalformedURLException e) {
            e.printStackTrace();
            System.exit(1);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }

        int numThreads = getNumProcessors();
        numActiveThreads = numThreads;

        tic = System.currentTimeMillis();

        Collection<Callable<Object>> apps = new ArrayList<Callable<Object>>();

        for (int thread = 0; thread < numThreads; thread++) {
            int step = calcStepSize(original.getWidth(), numThreads, thread);
            apps.add(new BlurImage(original, processed, thread * step, (thread + 1) * step, render));
        }

        try {
            getThreadPool().invokeAll(apps);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

The speed-up I experienced using two threads instead of one is close to optimal, meaning the time it took to calculate the blurred image took almost twice the amount of time with one thread compared to the time taken with two threads.

Advertisements