Как работает JComponent.paintImmediately() в Java Swing?

Мое понимание: В отличие от большинства компонентов/операций в вызове Swing для JComponent.repaint() является потокобезопасным, т.е. Хотя запрос перерисовки выполняется из другого потока (то есть не из EDT), фактическая картина происходит только в EDT. Ниже приведен фрагмент кода.

public class PaintingDemo {

    public static void main(String[] args) {
        final JFrame frame = new JFrame();
        final JPanel p = new MyPanel();
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                frame.add(p, BorderLayout.CENTER);
                frame.setSize(200, 200);
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setVisible(true);
            }
        });
        new Thread("MyThread") {
            public void run() {
                while (true) {
                // Below statements are important to show the difference
                    p.repaint();
                    p.paintImmediately(p.getBounds());
                    try {
                        Thread.sleep(1000);
                    } catch(Exception e) {}
                }
            }
        }.start();
    }

}

class MyPanel extends JPanel {
    @Override
    public void paint(Graphics g) {
        System.out.println("paint() called in "+ Thread.currentThread().getName());
        super.paint(g);
    }
}

Из вывода известно, что рисование выполняется в EDT при вызове repaint(), независимо от того, из какого потока он вызван, поэтому проблем нет. Но в случае paintImmediately() - живопись происходит в том же потоке, из которого он вызывается.

Рассмотрим случай, когда EDT изменяет состояние компонента и другой поток (из которого вызывается paintImmediately()) рисует тот же компонент.

Мой вопрос: В случае paintImmediately(), как выполняется синхронизация между потоком Dispatcher потока (EDT) и другими потоками?

Ответ 1

Насколько я понимаю, когда вы вызываете paintImmediately, вы вызываете следующий код:

        Component c = this;
        Component parent;

        if(!isShowing()) {
            return;
        }

        JComponent paintingOigin = SwingUtilities.getPaintingOrigin(this);
        if (paintingOigin != null) {
            Rectangle rectangle = SwingUtilities.convertRectangle(
                    c, new Rectangle(x, y, w, h), paintingOigin);
            paintingOigin.paintImmediately(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
            return;
        }

        while(!c.isOpaque()) {
            parent = c.getParent();
            if(parent != null) {
                x += c.getX();
                y += c.getY();
                c = parent;
            } else {
                break;
            }

            if(!(c instanceof JComponent)) {
                break;
            }
    }
    if(c instanceof JComponent) {
        ((JComponent)c)._paintImmediately(x,y,w,h);
    } else {
        c.repaint(x,y,w,h);
    }

Итак, если это не будет JComponent, вы в конечном итоге вызываете _paintImmediately(), который заканчивает вызов paint(Graphics), как предполагает трассировку стека ниже (полученную из фрагмента кода, который я выложу в конце этого сообщения ):

Thread [pool-1-thread-1] (Suspended)    
    TestPaint$1.paint(Graphics) line: 23    
    TestPaint$1(JComponent).paintToOffscreen(Graphics, int, int, int, int, int, int) line: 5221 
    RepaintManager$PaintManager.paintDoubleBuffered(JComponent, Image, Graphics, int, int, int, int) line: 1482 
    RepaintManager$PaintManager.paint(JComponent, JComponent, Graphics, int, int, int, int) line: 1413  
    RepaintManager.paint(JComponent, JComponent, Graphics, int, int, int, int) line: 1206   
    TestPaint$1(JComponent)._paintImmediately(int, int, int, int) line: 5169    
    TestPaint$1(JComponent).paintImmediately(int, int, int, int) line: 4980 
    TestPaint$1(JComponent).paintImmediately(Rectangle) line: 4992  
    TestPaint$3.run() line: 50  
    ThreadPoolExecutor.runWorker(ThreadPoolExecutor$Worker) line: 1110  
    ThreadPoolExecutor$Worker.run() line: 603   
    Thread.run() line: 722  

Но если вы попытаетесь одновременно вызвать repaint() одновременно (из другого потока), вы увидите, что оба они запускаются одновременно (я попытался сделать шаг в коде с debuger, и рисование никогда не прекращалось в другой теме). что на уровне кода Java не так много синхронизации (по крайней мере, я ничего не мог обнаружить). Поэтому, если вы в конечном итоге модифицируете состояние компонента в EDT, я считаю, что результаты довольно непредсказуемы, и вы должны избегать такой ситуации всеми средствами.

Чтобы проиллюстрировать мою точку зрения, я попытался изменить состояние переменной в методе paint, добавить sleep, чтобы увеличить риск столкновений между двумя нитями (EDT и другой), и кажется очевидным, что синхронизация между двумя потоками (System.err.println() выводится null время от времени).

Теперь мне было бы интересно, почему вам нужно выполнить рисование сразу. Если вы не блокируете EDT, существует не так уж много веских причин для выполнения такой вещи.

Ниже приведен код, который я использовал для проверки этих вещей (довольно близко к тому, что было опубликовано в вопросе). Код предназначен только для того, чтобы попытаться понять, что происходит, а не показывать, как выполнять правильную покраску или любую хорошую практику Swing.

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Random;
import java.util.concurrent.Executors;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class TestPaint {

    protected void initUI() {
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setTitle(TestPaint.class.getSimpleName());
        final Random rand = new Random();
        final JPanel comp = new JPanel() {
            private String value;

            @Override
            public void paint(Graphics g) {
                value = "hello";
                super.paint(g);
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                g.setColor(new Color(rand.nextInt(256), rand.nextInt(256), rand.nextInt(256)));
                g.fillRect(0, 0, getWidth(), getHeight());
                if (SwingUtilities.isEventDispatchThread()) {
                    System.err.println("Painting in the EDT " + getValue());
                } else {
                    System.err.println("Not painting in EDT " + getValue());
                }
                value = null;
            }

            public String getValue() {
                return value;
            }
        };
        frame.add(comp);
        frame.setSize(400, 400);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
        Timer t = new Timer(1, new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                comp.repaint();
            }
        });
        t.start();
        Executors.newSingleThreadExecutor().execute(new Runnable() {

            @Override
            public void run() {
                while (true) {
                    comp.paintImmediately(comp.getBounds());
                }
            }
        });
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new TestPaint().initUI();
            }
        });
    }

}