简体   繁体   中英

Strange Concurrency Issue in Java Swing

I have a Jframe which is my application's window (appFrame in the following code) that contains a lot of logic and takes like 1 second or so to load. In the meantime I want to show my user a very nice loading frame (initFrame). However, when I run this code, the initFrame does appear but the text in a JLabel on it doesn't appear immediately - it actually doesn't appear at all in the brief moment till the app frame is loaded.

If i comment out all the appFrame, and only launch the initFrame, the text is loaded instantly, no waiting time at all. Why is this so? Might this be a concurrency issue?

public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() { //as per best practice for concurrency in swing - see http://docs.oracle.com/javase/tutorial/uiswing/concurrency/
        @Override
        public void run() {
            final JFrame initFrame = new InitFrame();
            initFrame.setVisible(true);
            final AppFrame appFrame = new AppFrame();
            appFrame.setVisible(true);
            initFrame.setVisible(false);
            initFrame.dispose();
        }
    });

}

I would separate the frames' creation into two threads. The first, initializing InitFrame . Running this thread and calling isShowing() on the InitFrame object. When it returns true, run the second thread to initialize and show AppFrame .

This will force a happens before relationship between the visibility of the two frames.

class Main {
    JFrame initFrame = null;
    AppFrame appFrame = null;

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
               initFrame = new InitFrame();
               initFrame.setVisible(true);
            }
        });

        while(!initFrame.isShowing()) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
            }
        }

        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                appFrame = new AppFrame();
                appFrame.setVisible(true);
                initFrame.setVisible(false);
                initFrame.dispose();
            }
        });

    }
}

Here's an example of what might be going wrong in your AppFrame .

You can run the test with threading:

java SplashTest true

or without

java SplashTest

When threading is enabled, you see the SplashFrame and AppFrame updating every 250ms, more or less.

When threading is not enabled, you get to see the SplashFrame with no components showing, the app 'hangs' for 4 seconds, then you see the AppFrame .

The example is somewhat contrived, but might give you some ideas.

Note that the SplashFrame has no 'direct' connection to the AppFrame . All communication is through the AppFrameWorkListener interface.

I've also put the 'work' in the AppFrame . But really if there is a lot of processing to be done it should be extracted out of the UI code, run in a separate Thread , and the AppFrame would be notified of progress by the task, in the same way as the SplashFrame currently is.

import javax.swing.*;

class SplashTest {

    static boolean useThread = false;

    public static void main(String[] args) {
        // Pass true at the command line to turn on threading.
        // No args, or any value other than true will turn off threading.
        if (args.length > 0) {
            useThread = new Boolean(args[0]);
        }
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                SplashFrame splashFrame = new SplashFrame();
                splashFrame.setVisible(true);
                new AppFrame(splashFrame).setVisible(true);
            }});
    }

    private static class BaseFrame extends JFrame {
        public BaseFrame() {
            setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
            setSize(200, 200);
            setTitle(getClass().getSimpleName());
        }
    }

    private static class SplashFrame extends BaseFrame implements AppFrameWorkListener {
        JLabel status;

        public SplashFrame() {
            setLocation(0, 0);
            status = new JLabel("Splash Frame");
            getContentPane().add(status);
        }

        public void appFrameWorkStart() {
            status.setText("Work started");
        }

        public void appFrameWorkProgress(long timeElapsed) {
            status.setText("Work has taken " + timeElapsed + "ms so far");
        }

        public void appFrameWorkDone() {
            // http://stackoverflow.com/questions/1234912/how-to-programmatically-close-a-jframe
            setVisible(false);
            dispose();
        }
    }

    private static class AppFrame extends BaseFrame {
        JLabel status;
        AppFrameWorkListener listener;

        public AppFrame(AppFrameWorkListener listener) {
            setLocation(200, 200);
            status = new JLabel("App Frame");
            getContentPane().add(status);

            this.listener = listener;

            // None of this 'heavy lifting' should be in a constructor.
            if (useThread) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        doLotsOfWork(4);
                    }
                }).start();
            } else {
                doLotsOfWork(4);
                onWorkDone();
            }
        }

        private void doLotsOfWork(int workLengthSeconds) {
            // We're starting. Ensure onWorkStart is called on the EDT,
            // as this method may be called from a different Thread.
            invokeOnWorkStartOnEDT();

            long start = System.currentTimeMillis();

            // Hammer the CPU for "workLengthSeconds" number of seconds.
            // And do some contrived progress reporting.
            long workLengthMs = workLengthSeconds * 1000;
            while (System.currentTimeMillis() - start < workLengthMs) {
                long innerStart = System.currentTimeMillis();
                // Consume 250ms CPU before issuing progress update.
                while (System.currentTimeMillis() - innerStart < 250);
                invokeOnWorkProgressOnEDT(System.currentTimeMillis() - start);
            }

            // We're done now. Ensure onWorkDone is called on the EDT,
            // as this method may be called from a different Thread.
            invokeOnWorkDoneOnEDT();
        }

        private void invokeOnWorkStartOnEDT() {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    onWorkStart();
                }
            });
        }

        private void invokeOnWorkProgressOnEDT(final long timeElapsed) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    onWorkProgress(timeElapsed);
                }
            });
        }

        private void invokeOnWorkDoneOnEDT() {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    onWorkDone();
                }
            });
        }

        private void onWorkStart() {
            status.setText("Work Started");
            if (null != listener) {
                // Tell someone who's interested in the work status.
                listener.appFrameWorkStart();
            }
        }

        private void onWorkProgress(long timeElapsed) {
            status.setText("Work has taken " + timeElapsed + "ms so far");
            if (null != listener) {
                // Tell someone who's interested in the work status.
                listener.appFrameWorkProgress(timeElapsed);
            }
        }

        private void onWorkDone() {
            status.setText("Work Done");
            if (null != listener) {
                // Tell someone who's interested in the work status.
                listener.appFrameWorkDone();
            }
        }
    }

    interface AppFrameWorkListener {
        public void appFrameWorkDone();
        public void appFrameWorkStart();
        public void appFrameWorkProgress(long timeElapsed);
    }

}

You Should use Java Thread and you can show an interactive Splash Screen (Custom made) to your user in the mean while while your code is generating whatever you want here is a tutorial just take a look

You should use Threads for good and efficient concurrency thats it

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM