Home > other >  Syncronization error - ArrayList accessed by multiple Threads
Syncronization error - ArrayList accessed by multiple Threads

Time:01-28

This is in java. My program's purpose is to take screen captures of the computer screen many times per second and find all pixels that have a special shade of red. Then, it finds the average location of all the red pixels.

To improve the efficiency of my image processing, I created 3 threads, each processing 1/4 of the pixels. These threads plus the original thread will thus process all the pixels. However, I keep getting an error in my avgLocation() method. It's a null pointer exception, and I assume it was because the other threads were changing the size of the list containing all the red pixels, which causes the program to access data of a pixel that isn't there. In an attempt to remedy this, I used .join on Thread1 and Thread2 after Thread2's code, then I used .join on Thread3 after its code segment. So, all the threads should be joined before I called the avgLocation method, but it still keeps giving a NullPointerException whenever a specific shade of red shows up on screen. Here is the stack trace

Exception in thread "main" java.lang.NullPointerException
    at Images.avgLocation(Images.java:151)
    at Images.processImage(Images.java:133)
    at Images.main(Images.java:169)

Line 151 is

                xSum  = list.get(i)[0];

Line 133 is Here is my code:

import java.awt.AWTException;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.util.ArrayList;

public class Images {
    //takes in an input image and a target color and a bound to check within, returns Point to target
        public static Point processImage(BufferedImage img, Color color, int error) throws InterruptedException  {
            long start = System.nanoTime();
            //bounds to check on color components, make sure all within [0,255]
            int redLower = color.getRed() - error;
            int redHigher =  color.getRed()   error;
            int greenLower = color.getGreen() - error;
            int greenHigher =  color.getGreen()   error;
            int blueLower = color.getBlue() - error;
            int blueHigher =  color.getBlue()   error;
            
            //place all components within [0,255]
            if (redLower < 0) redLower = 0;
            if (redHigher > 255) redHigher = 255;
            if (greenLower < 0) greenLower = 0;
            if (greenHigher > 255) greenHigher = 255;
            if (blueLower < 0) blueLower = 0;
            if (blueHigher > 255) blueHigher = 255;
            
            //create final variables to use for thread
            int redLowerF = redLower;
            int redHigherF =  redHigher;
            int greenLowerF = greenLower;
            int greenHigherF =  greenHigher;
            int blueLowerF = blueLower;
            int blueHigherF =  blueHigher;
            
            //data of image
            int[] pixels = ((DataBufferInt)img.getRaster().getDataBuffer()).getData();
            int width = img.getWidth();
            int height = img.getHeight();
            
            //stores locations
            ArrayList <Integer[]> locations = new ArrayList <>();
            for (int i = 0; i < pixels.length/4; i  ) {
                int p = pixels[i];
                              
                // get red
                int r = (p >> 16) & 0xff;
          
                // get green
                int g = (p >> 8) & 0xff;
          
                // get blue
                int b = p & 0xff;
                
                //check if all components within bounds
                if (r >= redLower && r<=redHigher && g>=greenLower && g <= greenHigher && b >= blueLower && b <= blueHigher) {              
                    Integer[] point = {i % height, i/height};
                    locations.add(point);
                }
            }
            Thread thread1 = new Thread( ()  -> {
                for (int i = pixels.length/4; i < pixels.length/2; i  ) {
                    int p = pixels[i];
                                  
                    // get red
                    int r = (p >> 16) & 0xff;
              
                    // get green
                    int g = (p >> 8) & 0xff;
              
                    // get blue
                    int b = p & 0xff;
                    
                    //check if all components within bounds
                    if (r >= redLowerF && r<=redHigherF && g>=greenLowerF && g <= greenHigherF && b >= blueLowerF && b <= blueHigherF) {                
                        Integer[] point = {i % height, i/height};
                        locations.add(point);
                    }
                }
            });
            thread1.start();
            Thread thread2 = new Thread( ()  -> {
                for (int i = pixels.length/2; i < pixels.length*3/4; i  ) {
                    int p = pixels[i];
                                  
                    // get red
                    int r = (p >> 16) & 0xff;
              
                    // get green
                    int g = (p >> 8) & 0xff;
              
                    // get blue
                    int b = p & 0xff;
                    
                    //check if all components within bounds
                    if (r >= redLowerF && r<=redHigherF && g>=greenLowerF && g <= greenHigherF && b >= blueLowerF && b <= blueHigherF) {                
                        Integer[] point = {i % height, i/height};
                        locations.add(point);
                    }
                }
            });
            thread2.start();
            thread1.join();
            thread2.join();
            Thread thread3 = new Thread( ()  -> {
                for (int i = pixels.length*3/4; i < pixels.length; i  ) {
                    int p = pixels[i];
                                  
                    // get red
                    int r = (p >> 16) & 0xff;
              
                    // get green
                    int g = (p >> 8) & 0xff;
              
                    // get blue
                    int b = p & 0xff;
                    
                    //check if all components within bounds
                    if (r >= redLowerF && r<=redHigherF && g>=greenLowerF && g <= greenHigherF && b >= blueLowerF && b <= blueHigherF) {                
                        Integer[] point = {i % height, i/height};
                        locations.add(point);
                    }
                }
            });
            thread3.start();
            thread3.join();
            long end = System.nanoTime();
            System.out.println((end-start)/1000000);
            return avgLocation(locations);  
        }
        //given 2D array of locations, finds average location of set and returns as point
        public static Point avgLocation (ArrayList<Integer[]> list) {
            //if no points in list, return an impossible point on screen
            if (list.size() == 0) {
                return new Point (-100, -100);
            }
            //coordinates of average location (set to 100 to reveal bug easily)
            int avgX = 100;
            int avgY = 100;
            
            int xSum = 0;
            int ySum = 0;
            
            //loop through array
            for (int i = 0; i < list.size(); i  ) {
                //for each location, add up coordinates
                xSum  = list.get(i)[0];
                ySum  = list.get(i)[1];
            }
            avgX = xSum/list.size();
            avgY = ySum/list.size();
            return new Point (avgX, avgY);
        }
        public static void main (String[] args) {
            Robot robot = null;
            try {
                robot = new Robot();
            } catch (AWTException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
            while (true) {
                try {
                    processImage(robot.createScreenCapture(new Rectangle(0,0,d.width,d.height)), new Color (255, 0, 0), 10);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } 
            }
        }
}

CodePudding user response:

Well, what's wrong with your thread synchronization is that you have none at all. Your attempts to avoid concurrent access to the ArrayList by using join() failed, because your still having thread1 and thread2 run concurrently. If you stop them from running at the same time, any problems with the ArrayList would disappear, but of course, you also wouldn’t have any benefit from using multiple threads at all.

Now, you could replace the list with a thread safe implementation or add manual synchronization for the access, but that’s not how you should do this. This synchronization adds an overhead which can be larger than any potential gain from calculating concurrently. After all, the arithmetic performed per pixel isn’t complex.

Instead, you should preferably let all threads use their own lists to which they can add without contention. Then, merge the lists after two threads completed their work.

To make this task easier, you should refactor the code to have one method having parameters to control the range to process, instead of having the same code four times.

Further, you can replace Integer[] with int[], to avoid boxing primitive values into objects.

// takes in an input image and a target color and a bound to check within,
// returns Point to target
public static Point processImage(BufferedImage img, Color color, int error)
    throws InterruptedException, ExecutionException  {

    long start = System.nanoTime();
    //bounds to check on color components, make sure all within [0,255]
    int redLower = color.getRed() - error;
    int redHigher =  color.getRed()   error;
    int greenLower = color.getGreen() - error;
    int greenHigher =  color.getGreen()   error;
    int blueLower = color.getBlue() - error;
    int blueHigher =  color.getBlue()   error;
    
    //place all components within [0,255]
    if (redLower < 0) redLower = 0;
    if (redHigher > 255) redHigher = 255;
    if (greenLower < 0) greenLower = 0;
    if (greenHigher > 255) greenHigher = 255;
    if (blueLower < 0) blueLower = 0;
    if (blueHigher > 255) blueHigher = 255;

    //data of image
    int[] pixels = ((DataBufferInt)img.getRaster().getDataBuffer()).getData();
    int width = img.getWidth();
    int height = img.getHeight();
    
    //stores locations
    ArrayList <int[]> locations = processPixels(pixels, height, 0, pixels.length, 1,
        redLower, redHigher, greenLower, greenHigher, blueLower, blueHigher);
    long end = System.nanoTime();
    System.out.println((end-start)/1000000);
    return avgLocation(locations);  
}
private static ArrayList<int[]> processPixels(
    int[] pixels, int height, int from, int to, int divide,
    int redLower, int redHigher, int greenLower, int greenHigher,
    int blueLower, int blueHigher)
    throws InterruptedException, ExecutionException {

    List<FutureTask<ArrayList<int[]>>> jobs = new ArrayList<>();
    while(divide > 0 && to - from > 1) {
        int divideF = --divide;
        int mid = (from   to) >>> 1;
        int jobFrom = from, jobTo = mid;
        FutureTask<ArrayList<int[]>> f = new FutureTask<>(() -> processPixels(
            pixels, height, jobFrom, jobTo, divideF,
            redLower, redHigher, greenLower, greenHigher, blueLower, blueHigher));
        new Thread(f).start();
        jobs.add(f);
        from = mid;
    }
    ArrayList<int[]> locations = new ArrayList<>();
    for (int i = from; i < to; i  ) {
        int p = pixels[i];
        // get red
        int r = (p >> 16) & 0xff;
        // get green
        int g = (p >> 8) & 0xff;
        // get blue
        int b = p & 0xff;
        //check if all components within bounds
        if (r >= redLower && r<=redHigher && g>=greenLower && g <= greenHigher
         && b >= blueLower && b <= blueHigher) {              
            int[] point = {i % height, i/height};
            locations.add(point);
        }
    }
    for(FutureTask<ArrayList<int[]>> j: jobs) locations.addAll(j.get());
    return locations;
}
//given 2D array of locations, finds average location of set and returns as point
public static Point avgLocation(ArrayList<int[]> list) {
    //if no points in list, return an impossible point on screen
    if (list.size() == 0) {
        return new Point (-100, -100);
    }
    //coordinates of average location (set to 100 to reveal bug easily)
    int avgX = 100, avgY = 100;
    
    int xSum = 0, ySum = 0;
    
    //loop through array
    for (int i = 0; i < list.size(); i  ) {
        //for each location, add up coordinates
        xSum  = list.get(i)[0];
        ySum  = list.get(i)[1];
    }
    avgX = xSum/list.size();
    avgY = ySum/list.size();
    return new Point(avgX, avgY);
}

The processPixels will do the job of processing the specified range, but first divide the work by halving the range and spawn new threads to call the same method with one half while keeping the other. Since the method executed by the other threads will also create new threads according to the divide, there will be 2divide threads working on the job. So pass 0 for single threaded, 1 for two threads, 2 for four threads, 3 for eight, and so on.

All threads will perform their work, adding to a local list, and add the result of the sub tasks afterwards, if any.


But note that a similar concept has been implemented in Java already, plus dynamic adaptation to the number of available CPU cores and actual workload. You get it for free when using

// takes in an input image and a target color and a bound to check within,
// returns Point to target
public static Point processImage(BufferedImage img, Color color, int error) {
    long start = System.nanoTime();
    //bounds to check on color components, make sure all within [0,255]
    int redLower = color.getRed() - error;
    int redHigher =  color.getRed()   error;
    int greenLower = color.getGreen() - error;
    int greenHigher =  color.getGreen()   error;
    int blueLower = color.getBlue() - error;
    int blueHigher =  color.getBlue()   error;
    
    //place all components within [0,255]
    if (redLower < 0) redLower = 0;
    if (redHigher > 255) redHigher = 255;
    if (greenLower < 0) greenLower = 0;
    if (greenHigher > 255) greenHigher = 255;
    if (blueLower < 0) blueLower = 0;
    if (blueHigher > 255) blueHigher = 255;

    //create final variables to use for function
    int redLowerF = redLower;
    int redHigherF =  redHigher;
    int greenLowerF = greenLower;
    int greenHigherF =  greenHigher;
    int blueLowerF = blueLower;
    int blueHigherF =  blueHigher;

    //data of image
    int[] pixels = ((DataBufferInt)img.getRaster().getDataBuffer()).getData();
    int width = img.getWidth();
    int height = img.getHeight();

    List<int[]> locations = IntStream.range(0, pixels.length).parallel()
        .filter(i -> {
          int p = pixels[i];
          // get red
          int r = (p >> 16) & 0xff;
          // get green
          int g = (p >> 8) & 0xff;
          // get blue
          int b = p & 0xff;
          //check if all components within bounds
          return r >= redLowerF && r<=redHigherF && g>=greenLowerF && g <= greenHigherF
              && b >= blueLowerF && b <= blueHigherF;
        })
        .mapToObj(i -> new int[] {i % height, i / height})
        .collect(Collectors.toList());

    //stores locations
    long end = System.nanoTime();
    System.out.println((end-start)/1000000);
    return avgLocation(locations);  
}
//given 2D array of locations, finds average location of set and returns as point
public static Point avgLocation(List<int[]> list) {
    //if no points in list, return an impossible point on screen
    if (list.size() == 0) {
        return new Point (-100, -100);
    }
    //coordinates of average location (set to 100 to reveal bug easily)
    int avgX = 100, avgY = 100;
    int xSum = 0, ySum = 0;

    //loop through array
    for (int i = 0; i < list.size(); i  ) {
        //for each location, add up coordinates
        xSum  = list.get(i)[0];
        ySum  = list.get(i)[1];
    }
    avgX = xSum/list.size();
    avgY = ySum/list.size();
    return new Point(avgX, avgY);
}

We can improve the code even more, as we do not need to express the points as int[] arrays, when we know the relationship between indices and coordinates:

public static Point processImage(BufferedImage img, Color color, int error) {
    long start = System.nanoTime();
    //bounds to check on color components, make sure all within [0,255]
    int redLower = color.getRed() - error;
    int redHigher =  color.getRed()   error;
    int greenLower = color.getGreen() - error;
    int greenHigher =  color.getGreen()   error;
    int blueLower = color.getBlue() - error;
    int blueHigher =  color.getBlue()   error;
    
    //place all components within [0,255]
    if (redLower < 0) redLower = 0;
    if (redHigher > 255) redHigher = 255;
    if (greenLower < 0) greenLower = 0;
    if (greenHigher > 255) greenHigher = 255;
    if (blueLower < 0) blueLower = 0;
    if (blueHigher > 255) blueHigher = 255;

    //create final variables to use for function
    int redLowerF = redLower;
    int redHigherF =  redHigher;
    int greenLowerF = greenLower;
    int greenHigherF =  greenHigher;
    int blueLowerF = blueLower;
    int blueHigherF =  blueHigher;

    //data of image
    int[] pixels = ((DataBufferInt)img.getRaster().getDataBuffer()).getData();
    int width = img.getWidth();
    int height = img.getHeight();

    int[] locations = IntStream.range(0, pixels.length).parallel()
        .filter(i -> {
          int p = pixels[i];
          // get red
          int r = (p >> 16) & 0xff;
          // get green
          int g = (p >> 8) & 0xff;
          // get blue
          int b = p & 0xff;
          //check if all components within bounds
          return r >= redLowerF && r<=redHigherF && g>=greenLowerF && g <= greenHigherF
              && b >= blueLowerF && b <= blueHigherF;
        })
        .toArray();

    //stores locations
    long end = System.nanoTime();
    System.out.println((end-start)/1000000);
    return avgLocation(locations, height);  
}
private static Point avgLocation(int[] locations, int height) {
    if (locations.length == 0) {
        return new Point (-100, -100);
    }
    //coordinates of average location (set to 100 to reveal bug easily)
    int avgX = 100, avgY = 100;
    int xSum = 0, ySum = 0;

    //loop through array
    for (int i: locations) {
        int x = i % height, y = i / height;
        //for each location, add up coordinates
        xSum  = x;
        ySum  = y;
    }
    avgX = xSum/locations.length;
    avgY = ySum/locations.length;
    return new Point(avgX, avgY);
}

CodePudding user response:

ArrayList is not thread safe

Create a method like

    static private void addToList (ArrayList<Integer[]> list, Integer [] p) {
        synchronized (list) {
            list.add(p);
        }
    }

which can be called in your Threads like

Image.addToList(locations, point);

CodePudding user response:

The threads concurrently modify and change the locations ArrayList by adding new elements. ArrayList is not suitable for such concurrent modification without some synchronisation. However, there is CopyOnWriteArrayList that can support concurrent modification without synchronisation.

Try replacing line:

ArrayList<Integer[]> locations= new ArrayList<>();

With the following line:

final List<Integer []> locations = new CopyOnWriteArrayList<>();
  •  Tags:  
  • Related