javamultithreadingswinggraphicsmandelbrot

Mutli Threaded Mandelbrot Set viewer with Java Swing


Attempting a multi-threaded Mandelbrot Set viewer in Java using Swing graphics. I've seen a few Mandelbrot Set programs and other multi threaded Swing apps that use BufferedImage and/or SwingWorker, but I'm wondering if this approach can work/be redeemed, and if it cannot, why not?

On click (zoom in) and on key press (zoom out) I'm opening threads to calculate color values in quadrants (upper left, upper right, lower left, lower right), and updating these values in private static Color[][] colors = new Color[n][n];. Then the repaint method is called on the component to draw from the colors array.

Are these threads executing concurrently with the Event Dispatch Thread, and if so, why aren't these threads occurring ahead of the Event Dispatch Thread Swing component drawing with SwingUtilities.invokeLater in place?

No error messages, just unpredictable redrawing behavior on click and key press. Thank you.

Mandelbrot.java

import java.awt.Color;
import java.awt.BorderLayout;
import java.awt.Graphics;
import javax.swing.SwingUtilities;
import javax.swing.JFrame;
import javax.swing.JPanel;

import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Stack;

public class Mandelbrot extends JPanel {
    private static double size = 4.0; // complex plane is size units in width and length
    private static int n    = 1000; // window is n x n in pixels
    private static int max  = 255; // iterate through hex color values
    private static double x0 = -size/2; // set window origin
    private static double y0 = size/2; // set window origin
    private static double xC = 0; // set complex plane origin
    private static double yC = 0; // set complex plane origin
    private static double scaleFactor = size/n; // translate complex plane to pixels
    private static Stack<double[]> origins = new Stack<double[]>(); // track origins for zooming out
    private static double zoom = 2.0; // modify to alter brightness scheme on zoom
    private static Color[][] colors = new Color[n][n];

    public Mandlebrot() {
        addMouseListener(new MouseAdapter() {
            public void mousePressed(MouseEvent e) {
                double[] center = new double[]{xC, yC}; // rescale
                origins.push(center);
                zoom = (zoom * 2) - (0.5 * zoom);
                xC = x0 + (scaleFactor * e.getX());
                yC = y0 - (scaleFactor * e.getY());
                size = size/2.0;
                scaleFactor = size/n;
                x0 = xC - (size/2);
                y0 = yC + (size/2);
                ComputeThread quadrant1 = new ComputeThread(n, zoom, x0, y0, 0, n/2, 0, n/2, max, scaleFactor, colors);
                ComputeThread quadrant2 = new ComputeThread(n, zoom, x0, y0, n/2, n, 0, n/2, max, scaleFactor, colors);
                ComputeThread quadrant3 = new ComputeThread(n, zoom, x0, y0, 0, n/2, n/2, n, max, scaleFactor, colors);
                ComputeThread quadrant4 = new ComputeThread(n, zoom, x0, y0, n/2, n, n/2, n, max, scaleFactor, colors);
                Thread thread1 = new Thread(quadrant1);
                Thread thread2 = new Thread(quadrant2);
                Thread thread3 = new Thread(quadrant3);
                Thread thread4 = new Thread(quadrant4);
                thread1.start();
                thread2.start();
                thread3.start();
                thread4.start();
                repaint(0, 0, getWidth(), getHeight());
            }
        });
        addKeyListener(new KeyAdapter() {
            public void keyPressed(KeyEvent e) {
                if (e.getKeyCode() == KeyEvent.VK_LEFT) {
                    double[] center = origins.pop(); // rescale
                    zoom = (zoom + (zoom / 3)) / 2;
                    xC = center[0];
                    yC = center[1];
                    size = size*2.0;
                    scaleFactor = size/n;
                    x0 = xC - (size/2);
                    y0 = yC + (size/2);
                    ComputeThread quadrant1 = new ComputeThread(n, zoom, x0, y0, 0, n/2, 0, n/2, max, scaleFactor, colors);
                    ComputeThread quadrant2 = new ComputeThread(n, zoom, x0, y0, n/2, n, 0, n/2, max, scaleFactor, colors);
                    ComputeThread quadrant3 = new ComputeThread(n, zoom, x0, y0, 0, n/2, n/2, n, max, scaleFactor, colors);
                    ComputeThread quadrant4 = new ComputeThread(n, zoom, x0, y0, n/2, n, n/2, n, max, scaleFactor, colors);
                    Thread thread1 = new Thread(quadrant1);
                    Thread thread2 = new Thread(quadrant2);
                    Thread thread3 = new Thread(quadrant3);
                    Thread thread4 = new Thread(quadrant4);
                    thread1.start();
                    thread2.start();
                    thread3.start();
                    thread4.start();
                    repaint(0, 0, getWidth(), getHeight());
                }
            }
        });
    }
    
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {           
                g.setColor(colors[j][i]);
                g.drawLine(i, j, i, j);
            }
        }
    }

    public static int mand(Complex z0, int max) {
        Complex z = z0;
        for (int t = 0; t < max; t++) {
            if (z.abs() > zoom) {
                return t;
            }
            z = Complex.ad(Complex.mult(z, z), z0);
        }
        return max;
    }
    
    
    public static void main(String[] args) {

        
        for (int i = 0; i < n; i++) { // initialize array for initial drawing
            for (int j = 0; j < n; j++) {
                double a = x0 + (i * scaleFactor);
                double b = y0 - (j * scaleFactor);
                Complex z0 = new Complex(a, b);
                int gray = max - mand(z0, max);
                Color color = new Color(gray, gray, gray);
                colors[j][i] = color;
            }
        }

        
        SwingUtilities.invokeLater(() -> {
            var panel = new Threading();
            panel.setBackground(Color.WHITE);
            panel.setFocusable(true);
            var frame = new JFrame("Mandelbrot");
            frame.setSize(n, n);
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.getContentPane().add(panel, BorderLayout.CENTER);
            frame.setVisible(true);
        });
        
    }

}

ComputeThread.java

import java.awt.Color;

public class ComputeThread implements Runnable {
    
    private static int n;
    private static double x0;
    private static double y0;
    private static int xLow;
    private static int xHigh;
    private static int yLow;
    private static int yHigh;
    private static int max;
    private static double scaleFactor;
    private static double zoom;
    private static Color[][] colors = new Color[1000][1000];
    
    public ComputeThread(int n, double zoom, double x0, double y0, int xLow, int xHigh, int yLow, int yHigh, int max, double scaleFactor, Color[][] colors) {
        this.n = n;
        this.zoom = zoom;
        this.x0 = x0;
        this.y0 = y0;
        this.xLow = xLow;
        this.xHigh = xHigh;
        this.yLow = yLow;
        this.yHigh = yHigh;
        this.max = max;
        this.scaleFactor = scaleFactor;
        this.colors = colors;
    }

    @Override
    public void run() {
        for (int i = xLow; i < xHigh; i++) {
            for (int j = yLow; j < yHigh; j++) {
                double a = x0 + (i * scaleFactor);
                double b = y0 - (j * scaleFactor);
                Complex z0 = new Complex(a, b);
                int gray = max - mand(z0, max);
                Color color = new Color(gray, gray, gray);
                colors[j][i] = color;
            }
        }
    }

    public static int mand(Complex z0, int max) {
        Complex z = z0;
        for (int t = 0; t < max; t++) {
            if (z.abs() > zoom) {
                return t;
            }
            z = Complex.ad(Complex.mult(z, z), z0);
        }
        return max;
    }
    
}

Complex.java

import java.lang.Math;

public class Complex {
    double real;
    double imag;
    public Complex(double real, double imag) {
        this.real = real;
        this.imag = imag;
    }
    public static Complex ad(Complex num1, Complex num2) {
        Complex num = new Complex(0.0, 0.0);
        num.real = num1.real + num2.real;
        num.imag = num1.imag + num2.imag;
        return(num);
    }
    
    public static Complex mult(Complex num1, Complex num2) {
        Complex num = new Complex(0.0, 0.0);
        num.real = num1.real * num2.real - num1.imag * num2.imag;
        num.imag = num1.real * num2.imag + num1.imag * num2.real;
        return(num);
    }
    
    public double abs() {
        return Math.hypot(real, imag);
    }
}

Solution

    1. You made a mistake by using static fields in ComputeThread. That way threads use the same arrays and they overwrite each other. So you get 1/4 of your image.

    2. Repaint runs concurrently with threads so if it finishes earlier, not the whole image is drawn. You have to inform JFrame about finished calculations so it can repaint when all is ready.

    Below the code that works better:

    Mandelbrot.java

    package mb;
    
    import java.awt.Color;
    import java.awt.BorderLayout;
    import java.awt.Graphics;
    import javax.swing.SwingUtilities;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    
    import java.awt.event.MouseAdapter;
    import java.awt.event.MouseEvent;
    import java.awt.event.KeyAdapter;
    import java.awt.event.KeyEvent;
    import java.util.EventListener;
    import java.util.Stack;
    
    public class Mandelbrot extends JPanel implements CalcListener {
        private static double size = 4.0; // complex plane is size units in width and length
        private static int n    = 1000; // window is n x n in pixels
        private static int max  = 255; // iterate through hex color values
        private static double x0 = -size/2; // set window origin
        private static double y0 = size/2; // set window origin
        private static double xC = 0; // set complex plane origin
        private static double yC = 0; // set complex plane origin
        private static double scaleFactor = size/n; // translate complex plane to pixels
        private static Stack<double[]> origins = new Stack<double[]>(); // track origins for zooming out
        private static double zoom = 2.0; // modify to alter brightness scheme on zoom
        private static Color[][] colors = new Color[n][n];
        static Mandelbrot me;
    
        public Mandelbrot() {
            this.me=this;
            addMouseListener(new MouseAdapter() {
                public void mousePressed(MouseEvent e) {
                    double[] center = new double[]{xC, yC}; // rescale
                    origins.push(center);
                    zoom = (zoom * 2) - (0.5 * zoom);
                    xC = x0 + (scaleFactor * e.getX());
                    yC = y0 - (scaleFactor * e.getY());
                    size = size/2.0;
                    scaleFactor = size/n;
                    x0 = xC - (size/2);
                    y0 = yC + (size/2);
                    ComputeThread quadrant1 = new ComputeThread(n, zoom, x0, y0, 0, n/2, 0, n/2, max, scaleFactor, colors, me);
                    ComputeThread quadrant2 = new ComputeThread(n, zoom, x0, y0, n/2, n, 0, n/2, max, scaleFactor, colors, me);
                    ComputeThread quadrant3 = new ComputeThread(n, zoom, x0, y0, 0, n/2, n/2, n, max, scaleFactor, colors, me);
                    ComputeThread quadrant4 = new ComputeThread(n, zoom, x0, y0, n/2, n, n/2, n, max, scaleFactor, colors, me);
                    Thread thread1 = new Thread(quadrant1);
                    Thread thread2 = new Thread(quadrant2);
                    Thread thread3 = new Thread(quadrant3);
                    Thread thread4 = new Thread(quadrant4);
                    thread1.start();
                    thread2.start();
                    thread3.start();
                    thread4.start();
                    repaint(0, 0, getWidth(), getHeight());
                }
            });
            addKeyListener(new KeyAdapter() {
                public void keyPressed(KeyEvent e) {
                    if (e.getKeyCode() == KeyEvent.VK_LEFT) {
                        double[] center = origins.pop(); // rescale
                        zoom = (zoom + (zoom / 3)) / 2;
                        xC = center[0];
                        yC = center[1];
                        size = size*2.0;
                        scaleFactor = size/n;
                        x0 = xC - (size/2);
                        y0 = yC + (size/2);
                        ComputeThread quadrant1 = new ComputeThread(n, zoom, x0, y0, 0, n/2, 0, n/2, max, scaleFactor, colors,me);
                        ComputeThread quadrant2 = new ComputeThread(n, zoom, x0, y0, n/2, n, 0, n/2, max, scaleFactor, colors,me);
                        ComputeThread quadrant3 = new ComputeThread(n, zoom, x0, y0, 0, n/2, n/2, n, max, scaleFactor, colors,me);
                        ComputeThread quadrant4 = new ComputeThread(n, zoom, x0, y0, n/2, n, n/2, n, max, scaleFactor, colors,me);
                        Thread thread1 = new Thread(quadrant1);
                        Thread thread2 = new Thread(quadrant2);
                        Thread thread3 = new Thread(quadrant3);
                        Thread thread4 = new Thread(quadrant4);
                        thread1.start();
                        thread2.start();
                        thread3.start();
                        thread4.start();
                        repaint(0, 0, getWidth(), getHeight());
                    }
                }
            });
        }
        
        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {           
                    g.setColor(colors[j][i]);
                    g.drawLine(i, j, i, j);
                }
            }
        }
    
        public static int mand(Complex z0, int max) {
            Complex z = z0;
            for (int t = 0; t < max; t++) {
                if (z.abs() > zoom) {
                    return t;
                }
                z = Complex.ad(Complex.mult(z, z), z0);
            }
            return max;
        }
        
        
        public static void main(String[] args) {
    
            
            for (int i = 0; i < n; i++) { // initialize array for initial drawing
                for (int j = 0; j < n; j++) {
                    double a = x0 + (i * scaleFactor);
                    double b = y0 - (j * scaleFactor);
                    Complex z0 = new Complex(a, b);
                    int gray = max - mand(z0, max);
                    Color color = new Color(gray, gray, gray);
                    colors[j][i] = color;
                }
            }
    
            
            SwingUtilities.invokeLater(() -> {
                var panel = new Mandelbrot();
                panel.setBackground(Color.WHITE);
                panel.setFocusable(true);
                var frame = new JFrame("Mandelbrot");
                frame.setSize(n, n);
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.getContentPane().add(panel, BorderLayout.CENTER);
                frame.setVisible(true);
            });
            
        }
        
        public void calcDoneEvent()  
        {
          repaint();
        }
    }
    

    ComputeThread.java

    package mb;
    
    import java.awt.Color;
    
    public class ComputeThread implements Runnable {
        
        private  int n;
        private  double x0;
        private  double y0;
        private  int xLow;
        private  int xHigh;
        private  int yLow;
        private  int yHigh;
        private  int max;
        private  double scaleFactor;
        private  double zoom;
        private  Color[][] colors = new Color[1000][1000];
        private CalcListener listener;
        
        public ComputeThread(int n, double zoom, double x0, double y0, int xLow, int xHigh, int yLow, int yHigh, int max, double scaleFactor, Color[][] colors,CalcListener l) {
            this.n = n;
            this.zoom = zoom;
            this.x0 = x0;
            this.y0 = y0;
            this.xLow = xLow;
            this.xHigh = xHigh;
            this.yLow = yLow;
            this.yHigh = yHigh;
            this.max = max;
            this.scaleFactor = scaleFactor;
            this.colors = colors;
            this.listener = l;
        }
    
        @Override
        public void run() {
            for (int i = xLow; i < xHigh; i++) {
                for (int j = yLow; j < yHigh; j++) {
                    double a = x0 + (i * scaleFactor);
                    double b = y0 - (j * scaleFactor);
                    Complex z0 = new Complex(a, b);
                    int gray = max - mand(z0, max);
                    Color color = new Color(gray, gray, gray);
                    colors[j][i] = color;
                }
            }
            listener.calcDoneEvent();
        }
    
        public  int mand(Complex z0, int max) {
            Complex z = z0;
            for (int t = 0; t < max; t++) {
                if (z.abs() > zoom) {
                    return t;
                }
                z = Complex.ad(Complex.mult(z, z), z0);
            }
            return max;
        }
        
    }
    

    CalcListener.java

    package mb;
    
    public interface CalcListener
    {
      public void calcDoneEvent();
    }
    

    And Complex.java is without modifications.

    Another issue is coding style. I made a shortcut by putting static "me" variable in Mandelbrot which is bad practice. Correctly, you should rather inherit Mouse and Key Adapters to own classes that would be capable of passing listener to threads.