同步错误 - ArrayList 被多个线程访问

Syncronization error - ArrayList accessed by multiple Threads

这是在 java。我的程序的目的是每秒多次截取计算机屏幕的屏幕截图,并找到所有具有特殊红色阴影的像素。然后,它找到所有红色像素的平均位置。

为了提高我的图像处理效率,我创建了3个线程,每个线程处理1/4的像素。这些线程加上原始线程将因此处理所有像素。但是,我的 avgLocation() 方法一直出错。这是一个空指针异常,我认为这是因为其他线程正在更改包含所有红色像素的列表的大小,这导致程序访问不存在的像素数据。为了解决这个问题,我在 Thread2 的代码之后对 Thread1 和 Thread2 使用了 .join,然后在 Thread3 的代码段之后使用了 .join。因此,在我调用 avgLocation 方法之前应该连接所有线程,但只要屏幕上出现特定的红色阴影,它仍然会不断给出 NullPointerException。这是堆栈跟踪

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)

第 151 行是

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

第 133 行是 这是我的代码:

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();
                } 
            }
        }
}

线程通过添加新元素并发地修改和更改locations ArrayListArrayList 不适合这种没有同步的并发修改。 不过有CopyOnWriteArrayList可以支持不同步的并发修改

尝试替换行:

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

用下面一行:

final List<Integer []> locations = new CopyOnWriteArrayList<>();

ArrayList 不是线程安全的

创建一个类似

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

可以在您的线程中调用,例如

Image.addToList(locations, point);

好吧,你的线程同步有什么问题是你有 none。您试图通过使用 join() 来避免并发访问 ArrayList 的尝试失败了,因为您仍然同时拥有 thread1thread2 运行。如果您同时从 运行ning 停止它们,ArrayList 的任何问题都会消失,但当然,您也不会从使用多个线程中获得任何好处。

现在,您可以用线程安全实现替换列表或为访问添加手动同步,但这不是您应该这样做的方式。这种同步增加了一个开销,该开销可能大于并发计算的任何潜在收益。毕竟,每个像素执行的算法并不复杂。

相反,您最好让所有线程使用它们自己的列表,它们可以无竞争地添加到列表中。然后,在两个线程完成工作后合并列表。

为了简化此任务,您应该重构代码,使一个方法具有参数来控制要处理的范围,而不是四次使用相同的代码。

此外,您可以将 Integer[] 替换为 int[],以避免将原始值装箱到对象中。

// 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);
}

processPixels 将完成处理指定范围的工作,但首先 divide 将范围减半并生成新线程以用一半调用相同的方法,同时保留另一半.由于其他线程执行的方法也会根据divide创建新的线程,因此会有2divide个线程在工作。因此,为单线程传递 0,为两个线程传递 1,为四个线程传递 2,为八个线程传递 3,依此类推。

所有线程都将执行它们的工作,添加到本地列表,然后添加子任务的结果(如果有的话)。


但请注意,类似的概念已在 Java 中实现,并且动态适应可用的 CPU 核心数量和实际工作负载。使用时免费获得

// 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);
}

我们可以进一步改进代码,因为我们不需要将点表示为 int[] 数组,当我们知道索引和坐标之间的关系时:

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);
}