Threads, Animation and Images

Contents

 

 

Threads

 

synchronization

 

animation

 

Loading and generating images

 

Practice exercises

Threads

Threads are seemingly parallel parts of a program that access the same data. Two separate programs running in parallel are called processes. Our previous graphics programs already ran in parallel, but we didn't utilize this. One thread was the `main` program , and the other was the event handler . However, our program has no control over the `main` thread because it doesn't own one. Therefore, this thread cannot be interrupted or waited. The `main` thread naturally terminates when the `main` subroutine finishes. The program itself can continue running. Only when the last window is closed does the event handler, and thus the program, terminate. Alternatively, the program can be completely terminated at any time using `System.exit(0)` .

A typical application for multi-threading is animated programs. We'll first give an example where the current time ticks in a window. This is achieved by starting a thread that repaints the applet every second.

The one-second interval is generated by the method The `sleep()` method is called by the thread. This method may generate an exception (if interruption was not possible), which must be caught.

If you were to simply draw in the window, the string would be deleted each time before it could be redrawn. The user would see a flickering effect. Therefore, we use a double buffer . Essentially, an image is created, which is then drawn onto, and only then is the image copied to the screen. Java does this automatically for JPanel if `setDoubleBuffered(true)` is set. However, you then have to clear the background yourself.

 

import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Date;

import javax.swing.JFrame;
import javax.swing.JPanel;

/**
 * A thread that becomes active every 1000 ms.
 */
class RedrawThread extends Thread
{
    JPanel canvas;
    boolean stop = false;

    public RedrawThread (JPanel canvas)
    {
        this.canvas = canvas;
        start();
    }

    @Override
    public void run()
    {
        while (!stop)
        {
            Try // Necessary!
            {
                sleep(1000); // Wait 1 second
            }
            catch (Exception ex)
            {
            }
            canvas.repaint();
        }
    }
}

class Display extends JPanel
{
    public Display ()
    {
        setPreferredSize(new Dimension(300, 100));

        // Set panel to Double Buffered
        setDoubleBuffered(true);
    }

    @Override
    public void paint (Graphics g)
    {
        g.setFont(new Font("Dialog", Font.BOLD, 16));
        FontMetrics metrics = g.getFontMetrics();
        String s = new Date().toString();

        // Due to double buffering, we have to delete manually.
        int w = getWidth();
        int h = getHeight();
        g.clearRect(0, 0, w, h);

        // Calculate string width and height
        int ws = metrics.stringWidth(s);
        int hs = metrics.getHeight();

        // Calculate starting point for string
        int x = (w - ws) / 2;
        int y = (h - hs) / 2 + metrics.getAscent();

        g.drawString(s, x, y);
    }
}

public class Test extends JFrame
{
    RedrawThread redraw;

    public Test()
    {
        super("Clock");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setIconImage(new javax.swing.ImageIcon(
                getClass().getResource("hearts32.png")).getImage());

        Display display = new Display();
        add("Center", display);

        redraw = new RedrawThread(display);

        addWindowListener(new WindowAdapter()
        {
            @Override
            public void windowClosing(WindowEvent e)
            {
                redraw.stop = true;
            }
        });

        pack();
        setLocationRelativeTo(null);
        setVisible(true);
    }

    public static void main (String args[])
    {
        new Test();
    }
}

The thread should be paused when the window is closed. To achieve this, the thread is given a variable that motivates it to pause. This variable is set when the frame is closed. Directly pausing the thread would be unsafe and is not possible in Java.

FontMetrics was used again to center the time display . We also added an icon to the window.

synchronization

We would like to point out that problems can occur when multiple threads access the same object or variable. The threads cannot rely on the object remaining unchanged for a specific period of time. Such problems must be addressed through synchronization .

To achieve this, a block can be synchronized with an object. Within this block, the thread then has a so-called lock on the object. No other thread can enter the block until the object is released. Such a block looks like this.

synchronized ( object )
 statement block

Alternatively, you can equip an entire method of a class with a lock on the instance of the class whose method was called. This is done by adding the `synchronized` keyword to the method.

Example

    public synchronized int inc ()
    {   
        int m=n;
        n++;
        return m;
    }

This example increments n and returns the old value. If n is a variable of object A, then A.inc() can only be active in one thread at a time. This prevents the value of n in A from becoming inconsistent. It is also always guaranteed that n++ will not be interrupted. However, the crucial point here is that the retrieval of the old value and the incrementing of the value are not interrupted by accesses from other threads.

Another problem is race conditions . You never know where the other thread is or whether it has just started. You might want to interrupt it, but it hasn't even started yet. This is also why the thread in the example above isn't simply interrupted, but instead a variable called ` stop` is set.

However, you can use `wait` and `notify` to wait for the thread to complete its work. Both methods must be called within a `synchronized` block. This makes the objects the thread's owner. Additionally, `wait` can throw an exception, which should be caught.

The `wait` and `notify` mechanisms are designed for synchronized communication between threads. In the following example, we have a producer and a consumer . The producer simply generates the numbers 1 through 10. However, it could also be input from an external device that needs to be passed on. Once it has generated a number, it passes it to the consumer by modifying a variable there. The consumer then simply outputs the number.

For synchronization to work, the consumer waits with "wait" for the producer , who sends a "notify" after production . Once the number has been processed, the consumer waits again with "wait" and notifies the producer.

class Producer implements Runnable
{
    Consumer;
    boolean ended;

    public producer (consumer consumer)
    {
        this.consumer = consumer;
        ended = false;
        new Thread(this).start();
    }

    @Override
    public void run()
    {
        for (int i = 0; i < 10; i++)
        {
            consumer.current = i;
            System.out.println(i + " produced");
            synchronized (consumer)
            {
                consumer.notify();
            }
            synchronized (this)
            {
                try
                {
                    wait();
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
        }

        ended = true;
    }
}

class Consumer implements Runnable
{
    int current;
    Producer;

    public void setProducer (Producer producer)
    {
        this.producer = producer;
        new Thread(this).start();
    }

    @Override
    public void run()
    {
        while (!producer.ended)
        {
            synchronized (this)
            {
                try
                {
                    wait();
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
            System.out.println(current + " received");
            synchronized (producer)
            {
                producer.notify();
            }
        }
    }

}

public class Test
{
    public static void main (String args[])
    {
        Consumer consumer = new Consumer();
        Producer producer = new Producer(consumer);
        consumer.setProducer(producer);
    }
}

The program outputs the following result.

0 produced
0 received
1 produced
1 received
2 produced
2 Received
3 produced
3 received
4 produced
4 received
5 produced
5 received
6 produced
6 received
7 produced
7 received
8 produced
8 received
9 produced
9 received

If you want to run multiple threads in parallel, executors are a good option. The following program demonstrates how to create and run two threads and wait for them to finish.

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

class Task implements Runnable
{

    @Override
    public void run()
    {
        for (int i = 0; i < 10; i++)
            System.out.println(i);
    }

}

public class Test
{
    public static void main (String args[])
    {
        System.out.println("Main started");

        ThreadPoolExecutor executor = //
                (ThreadPoolExecutor) Executors.newFixedThreadPool(2);

        for (int i = 0; i < 2; i++)
        {
            Task t = new Task();
            executor.execute(t);
        }
        executor.shutdown();

        try
        {
            executor.awaitTermination(10, TimeUnit.SECONDS);
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }

        System.out.println("Main ended");
    }
}

The output is as expected.

Main started
0
1
2
3
4
5
6
7
0
1
2
3
4
5
6
7
8
9
8
9
Main end

The threads appear to be working in parallel, since the numbers are being output sequentially. Note that ` System.out.println` is synchronized. Therefore, nothing can go wrong even when outputting longer strings. The main program will only terminate once all numbers have been output.

animation

In this section, we will write a program where a ball bounces within the canvas area. The approach is to run a separate thread that recalculates the ball's position and redraws the ball.

A few words of explanation are necessary, as this is a very complex program.

For the rendering, we again use a JPanel with a double buffer . We could also prevent the background from being redrawn by overriding the `update(Graphics g)` method and simply calling `paint(g)` ourselves . In this example, you'll hardly see a difference on a fast computer, since only the ball is being overdrawn by the background. But the double buffer adds very little processing time.

Instead of directly creating a thread , it makes more sense to implement the Runnable interface . It's easier to use; the thread starts when the window receives focus.

This program should stop the animation when the window loses focus. The simplest and safest way is to stop the thread . When focus returns, we start a new thread that resumes the animation from the same point. There are also methods in `run()` that allow you to wait for an event that you have to create yourself.

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.JFrame;
import javax.swing.JPanel;

class BallCanvas extends JPanel implements Runnable
{
    boolean stop;

    int x, y, dx, dy; // Ball coordinates seen from the bottom left
    int w, h; // Height and width of the applet
    int s; // ball size

    /**
     * Initialize
     */
    public BallCanvas ()
    {
        setPreferredSize(new Dimension(800, 600));
        x = 400;
        y = 10;
        dx = 2;
        dy = 30;
        s = 20;

        setDoubleBuffered(true);
    }

    /**
     Draw the background. Draw the ball in the correct position.
     */
    @Override
    public void paint (Graphics g)
    {
        w = getWidth();
        h = getHeight();
        g.setColor(Color.blue.darker());
        g.fillRect(0, 0, w, h);
        g.setColor(Color.green.darker());
        g.fillOval(x - s, h - y - s, 2 * s, 2 * s);
    }

    /**
     * Loop of the thread. The ball bounces with gravity and loses momentum.
     * Each impact absorbs energy until it just rolls.
     */
    @Override
    public void run()
    {
        while (!stop)
        {
            // Move the ball:
            if (x + dx - s <= 0)
                dx = -dx;
            if (x + dx + s >= w)
                dx = -dx;
            if (y + dy - s <= 0)
            {
                dy = -dy - 1;
                y = s;
            }
            else
                dy--;
            x += dx;
            y += dy;

            repaint();
            // To delay something
            try
            {
                Thread.sleep(10);
            }
            catch (Exception e)
            {
            }
        }
    }

}

public class Test extends JFrame implements FocusListener
{
    BallCanvas ball;
    boolean stop;

    public Test()
    {
        super("Ball Animation");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setIconImage(new javax.swing.ImageIcon(
                getClass().getResource("hearts32.png")).getImage());

        ball = new BallCanvas();
        add("Center", ball);

        addWindowListener(new WindowAdapter()
        {
            @Override
            public void windowClosing(WindowEvent e)
            {
                ball.stop = true;
            }
        });

        addFocusListener(this);

        pack();
        setLocationRelativeTo(null);
        setVisible(true);
    }

    public static void main (String args[])
    {
        new Test();
    }

    // Routines for FocusListener

    @Override
    public void focusGained (FocusEvent e)
    {
        ball.stop = false;
        new Thread(ball).start();

    }

    @Override
    public void focusLost(FocusEvent e)
    {
        ball.stop = true;
    }

}

Loading images andGenerate

We now want to enhance our bouncing ball by loading the background from a file and rendering the ball itself. Loading the file is easy if the file path is known.

BufferedImage img = null;
try
    {
        img = ImageIO.read(new File("file.jpg"));
    } catch (IOException e) {
}

Java programs are often packaged in JAR files, including the images, which are then called resources . The necessary code is included in the example.

The ball in the example above is a partially transparent image, rendered pixel by pixel . This is achieved using the `MemoryImageSource` class from the `java.awt.image` package . An object of this class can be passed to `createImage` to create the image (in this case, the image "Ball"). A `MemoryImageSource` is created from individual pixels stored in an `int` array. Each pixel consists of four bytes that specify the transparency and the intensity of the three primary colors (RGB) for the pixel (each ranging from 0 to 255, where 255 represents "not transparent"). The best way to assemble the pixel is using shift operations and a bitwise OR (`|`).

It's also interesting to see how the color components for the pixels are determined. First, the two-dimensional top-down coordinate (i,j) of the sphere is converted into the three-dimensional surface coordinate (x,y,z). This is then multiplied by (1,1,1) using a scalar multiplication function. The resulting value, correctly scaled, serves as the brightness at that point. This simple mathematical trick convincingly simulates light coming from the upper right front.

Details can be found in the following listing for the createBall() function.

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.ColorModel;
import java.awt.image.MemoryImageSource;

import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;

class BallCanvas extends JPanel implements Runnable
{
    boolean stop;

    Image I; // Buffer
    Image Back; // Background
    Image Ball; // Ball
    Graphics G; // Graphics for the buffer

    int x, y, dx, dy; // Ball coordinates seen from the bottom left
    int w, h; // Height and width of the applet
    final int s; // Ball size as a constant

    public BallCanvas ()
    {
        try
        {
            Back = ImageIO.read(
                    getClass().getClassLoader().getResource("tile.png"));
        }
        catch (Exception e)
        {
        }

        w = Back.getWidth(this);
        h = Back.getHeight(this);
        setPreferredSize(new Dimension(w, h));

        x = w / 2;
        s = h / 5;
        y = s;
        dx = 2;
        dy = (int) Math.sqrt(2 * h);

        Ball = createBall(s);

        setDoubleBuffered(true);
    }

    /**
     * Create a rendered ball.
     *
     * @param S size
     * @return
     */
    Image createBall (int S)
    {
        int i, j, k;
        int P[] = new int[4 * (S + 1) * (S + 1)]; // for the pixels
        k = 0;
        double red, green, blue, light, x, y, z;

        for (i = -S; i <= S; i++)
            for (j = -S; j <= S; j++)
            { // Calculate x,y,z coordinates on the ball surface
                x = -(double) i / S;
                y = (double) j / S;
                z = 1 - x * x - y * y;

                if (z <= 0)
                    P[k] = 0;
                // Outside the ball! Transparent dot.
                else
                {
                    z = Math.sqrt(z);
                    light = (x + y + z) / Math.sqrt(3) * 0.4;
                    // Vector product with 1,1,1
                    red = 0.6 * (1 + light); // Red content
                    green = 0.2 * (1 + light); // Green share
                    blue = 0; // Blue component
                    P[k] = 255 << 24 | // not transparent!
                            (int) (red * 255) << 16 |
                            (int) (green * 255) << 8 |
                            (int) (blue * 255);
                    // P[k] consists of four bytes of the
                    // Colors and transparency together
                }
                k++;
            }
        return createImage( // Create the image
                new MemoryImageSource(2 * S + 1, 2 * S + 1,
                        ColorModel.getRGBdefault(),
                        P, 0, 2 * S + 1));
    }

    /**
     Draw the background. Draw the ball in the correct position.
     */
    @Override
    public void paint (Graphics g)
    {
        g.drawImage(Back, 0, 0, this);
        g.drawImage(Ball, x - s, h - y - s, this);
    }

    /**
     * Loop of the thread. The ball bounces with gravity and loses momentum.
     * Each impact absorbs energy until it just rolls.
     */
    @Override
    public void run()
    {
        while (!stop)
        {
            // Move the ball:
            if (x + dx - s <= 0)
                dx = -dx;
            if (x + dx + s >= w)
                dx = -dx;
            if (y + dy - s <= 0)
            {
                dy = -dy - 1;
                y = s;
            }
            else
                dy--;
            x += dx;
            y += dy;

            repaint();
            // To delay something
            try
            {
                Thread.sleep(10);
            }
            catch (Exception e)
            {
            }
        }
    }

}

public class Test extends JFrame implements FocusListener
{
    BallCanvas ball;
    boolean stop;

    public Test()
    {
        super("Ball Animation");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setIconImage(new javax.swing.ImageIcon(
                getClass().getResource("hearts32.png")).getImage());

        ball = new BallCanvas();
        add("Center", ball);

        addWindowListener(new WindowAdapter()
        {
            @Override
            public void windowClosing(WindowEvent e)
            {
                ball.stop = true;
            }
        });

        addFocusListener(this);

        pack();
        setLocationRelativeTo(null);
        setVisible(true);
    }

    public static void main (String args[])
    {
        new Test();
    }

    // Routines for FocusListener

    @Override
    public void focusGained (FocusEvent e)
    {
        ball.stop = false;
        new Thread(ball).start();

    }

    @Override
    public void focusLost(FocusEvent e)
    {
        ball.stop = true;
    }

}

Of course, it is also possible to decompose an image into RGB values.

    static Image turn (Image I, Component c)
    {   
        int W=I.getWidth(c),H=I.getHeight(c);
        int P[]=new int [W*H];
        PixelGrabber pg=new PixelGrabber(I,0,0,W,H,P,0,W);
        try { pg.grabPixels(); } catch (Exception e) { return I; }
        int Q[]=new int[W*H];
        int i,j;
        for (i=0; i<H; i++)
            for (j=0; j<W; j++)
            {   
                Q[i*W+j]=P[i*W+j];                  
            }
        return Toolkit.getDefaultToolkit().createImage(
            new MemoryImageSource(H,W,ColorModel.getRGBdefault(),
                            Q,0,H));
    }

Example - Merge Sort with Threads

Since modern processors always have multiple threads, it makes sense to speed up the merge sort from this example using threads. To do this, we split the original pile into two parts and sort both in separate threads, without creating any additional threads. This speeds up the algorithm on the current computer used from 10 seconds to 6 seconds.

However, the implementation is considerably more complex. We need a class that implements Runnable and contains all the information about the parts to be sorted.

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * Helper class that can create a thread to sort a portion of the work.
 *
 * @param <Type>
 */
class SortRunner<Type extends Comparable<Type>> implements Runnable
{
    ThreadedMergeSorter<Type> sorter;
    Type[] array;

    int start, end;

    /**
     * We remember all parameters, since run() has no parameters
     *
     * @param sorter
     * @param array
     * @param start
     * @param end
     */
    public SortRunner (ThreadedMergeSorter<Type> sorter, Type[] array,
            int start,
            int end)
    {
        this.sorter = sorter;
        this.array = array;
        this.start = start;
        this.end = end;
    }

    @Override
    public void run()
    {
        sorter.sort(array, start, end);
    }
}

/**
 * Sorts from 10000 elements onwards by sorting two halves with threads.
 *
 * @param <Type>
 */
class ThreadedMergeSorter<Type extends Comparable<Type>>
{
    Type[] work;

    @SuppressWarnings("unchecked")
    public void sort (Type[] array)
    {
        if (array.length < 2)
            return;
        work = (Type[]) java.lang.reflect.Array.newInstance(array[0].getClass(),
                array.length);
        sortthreaded(array, 0, array.length - 1);
    }

    void sortthreaded (Type[] array, int start, int end)
    {
        if (end - start < 10000) // Array is small
        {
            sort(array, start, end);
        }
        else // Array is large
        {
            ThreadPoolExecutor executor = //
                    (ThreadPoolExecutor) Executors.newFixedThreadPool(2);

            int half = end / 2;

            SortRunner<Type> runner1 = new SortRunner<Type>(this, array, 0,
                    half);
            executor.execute(runner1);
            SortRunner<Type> runner2 = new SortRunner<Type>(this, array,
                    half + 1, end);
            executor.execute(runner2);
            executor.shutdown();

            try
            {
                executor.awaitTermination(100, TimeUnit.SECONDS);
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }

            merge(array, 0, half, half + 1, end);
        }
    }

    void sort (Type[] array, int start, int end)
    {
        // System.out.println(start + "-" + end);
        int half = (start + end) / 2;
        if (half - start > 0)
            sort(array, start, half);
        if (end - (half + 1) > 0)
            sort(array, half + 1, end);
        merge(array, start, half, half + 1, end);
    }

    void merge (Type[] array, int start1, int end1, int start2, int end2)
    {
        int aim = start1;
        int start = start1;
        while (true)
        {
            if (array[start1].compareTo(array[start2]) < 0)
            {
                work[aim++] = array[start1++];
                if (start1 > end1)
                {
                    while (start2 <= end2)
                        work[aim++] = array[start2++];
                    break;
                }
            }
            else
            {
                work[aim++] = array[start2++];
                if (start2 > end2)
                {
                    while (start1 <= end1)
                        work[aim++] = array[start1++];
                    break;
                }
            }
        }
        System.arraycopy(work, start, array, start, aim - start);
    }
}

public class Test
{
    public static void main (String args[]) throws Exception
    {
        int N = 10000000;
        String[] array = new String[N];
        for (int i = 0; i < N; i++)
            array[i] = "String " + Math.random();

        ThreadedMergeSorter<String> sorter = new ThreadedMergeSorter<String>();

        long time = System.currentTimeMillis();
        sorter.sort(array);
        System.out.println((System.currentTimeMillis() - time) + " msec");

        for (int i = 0; i < N - 1; i++)
            if (array[i].compareTo(array[i + 1]) > 0)
                throw new Exception("Error in Sort");
    }
}

 

 

Exercisetasks

  1. Rewrite the program that displays the time so that it shows a simple clock with hands.

Solutions .

Problems without solutions

  1. Create a Runnable object with an integer variable n. Have two threads increment this variable (with output to the screen) until it exceeds a certain value. Observe whether the output is chronologically correct. Are there any duplicate outputs? Are such duplicate outputs even conceivable?
  2. Write a program that scrolls text from left to right. Choose a font that is 2/3 the height of the window. The animation should be similar to the ball animation and buffered.
  3. Bounce multiple balls, with or without a collision test.

Back to the Java course