JDK-4735740 : Java Sound keeps a console app from exiting because of non-daemon event thread
  • Type: Bug
  • Component: client-libs
  • Sub-Component: javax.sound
  • Affected Version: 1.3.0,1.4.1,1.4.2
  • Priority: P4
  • Status: Resolved
  • Resolution: Fixed
  • OS:
    generic,windows_98,windows_2000,windows_xp generic,windows_98,windows_2000,windows_xp
  • CPU: generic,x86
  • Submitted: 2002-08-22
  • Updated: 2003-08-02
  • Resolved: 2003-08-02
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
Other
5.0 tigerFixed
Related Reports
Duplicate :  
Duplicate :  
Duplicate :  
Duplicate :  
Relates :  
Relates :  
Description

Name: rmT116609			Date: 08/22/2002


FULL PRODUCT VERSION :
java version "1.4.1-rc"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.4.1-rc-b19)
Java HotSpot(TM) Client VM (build 1.4.1-rc-b19, mixed mode)

FULL OPERATING SYSTEM VERSION :
Microsoft Windows 2000 [Version 5.00.2195]

A DESCRIPTION OF THE PROBLEM :
When a clip line is obtained, played, and closed, two
threads remain.
   1        "Java Sound event dispatcher"
   2 Daemon "Headspace mixer frame proc thread"

In a console application, the first thread, since it is not
a Daemon, will keep the application from exiting until that
thread is interrupted. This is not a problem in a Swing
application.

The problem is two-fold:
  1 Console application cannot exit properly unless they
find the thread by name and interrupt it. This is not a
solution that is sure to be portable across future Java
Sound API versions.
  2 When any type of Line is obtained, these thread
resources are allocated. They stay allocated for the
lifetime of the application. I only know of the threads at
this time but there may be other Java Sound resources that
are hanging around as well. This is poor resource
management. It is fine to allocate them at first need, but
there must also be an API to:
    * Detect if Java Sound resources are allocated.
    * Release Java Sound resources on demand as long as they
are not needed.
    * Pre-allocate Java Sound resources in anticipation of
their need.

The following simple console application demonstrates this
behavior. If not run with the "-i" option, the given sound
file will play but the application will not exit. The thread
dump will show the Java Sound threads, of which the
non-Daemon thread is the culprit. If it is run with the "-i"
option, the application will exit and the thread dump will
show the non-Daemon Java Sound thread no longer exists.

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. In a console application, get a
"javax.sound.sampled.Line", which will automatically
allocate Java Sound resources among which is the non-Daemon
"Java Sound event dispatcher" thread.
2. Try to exit the cosole application.


EXPECTED VERSUS ACTUAL BEHAVIOR :
Expected: The console application exits.
Actual  : The console application hangs until ctrl-c is pressed.

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------

import java.io.*;
import javax.sound.sampled.*;


/**
 * A simple sound file playing console application using the Java Sound API.
 *
 * @author Curtis Clauson for The Snake Pit - Development
 */
public
class PlaySound {
    /*
     * Class Methods
     */


    /*
     * Plays the given sound file.
     *
     * @args args The array of command line argument Strings.
     */
    public static
    void
    main(String[] args)
    {
        // Parse arguments.
        final int argCount = args.length;
        if (argCount == 0 || argCount > 2) {
            System.err.println(
                  "ERROR: Invalid number of arguments.\n"
                + "usage: java PlaySound [options] <wave file path>\n"
                + "Options:\n"
                + "  -i  Interrupt the \"Java Sound event dispatcher\" thread on
exit."
            );
            return;
        }

        boolean interruptThread = false;
        File    file = null;
        for (int iArg = 0; iArg < argCount; iArg++) {
            String arg = args[iArg];
            if (arg.equals("-i")) interruptThread = true;
            else {
                file = new File(arg);
                if (!file.exists() || file.isDirectory()) {
                    System.err.println("ERROR: File not found.");
                    return;
                }
            }
        }
        if (file == null) {
            System.err.println("ERROR: No sound file was given.");
            return;
        }


        // Play the sound file.
        PlaySound playWav = new PlaySound(file);
        playWav.play();

        // Deal with and show the active threads.
        System.out.println("Done.");
        if (interruptThread) {
            Thread thread = Utilities.getThread("Java Sound event dispatcher");
            if (thread != null) thread.interrupt();
        }
        Utilities.listThreads();
    }


    /*
     * Instance Data
     */

    /** The sound clip of the given file. */
    Clip clip;


    /*
     * Constructors
     */

    /*
     * Create a PlaySound class.
     */
    public
    PlaySound(final File file)
    {
        // Get an AudioInputStream.
        AudioInputStream is;
        try {
            is = AudioSystem.getAudioInputStream(file);
        } catch (UnsupportedAudioFileException e) {
            System.err.println("Unknown sampled file format.");
            return;
        } catch (IOException e) {
            System.err.println("Error reading the file.");
            return;
        }
        System.out.println("File: " + file.getName() + "  Frame Length: " +
is.getFrameLength() + "  Format: " + is.getFormat().getEncoding().toString());

        // Get a clip.
        try {
            clip = (Clip)AudioSystem.getLine(new Line.Info(Clip.class));
        } catch (LineUnavailableException e) {
            System.err.println("No Clip was available.");
            return;
        }
        System.out.println("Clip: " + clip.getLineInfo().toString());
        clip.addLineListener(new LineListener() {
            public void update(LineEvent event) {
                LineEvent.Type type = event.getType();
                System.out.println("*** Line Event: " + type.toString() + " ***");
                if (type == type.STOP) {
                    clip.close();
                } else if (type == type.CLOSE) {
                    synchronized(PlaySound.this) {
                        PlaySound.this.notify();
                    }
                }
            }
        });

        // Open and load the clip from the AudioInputStream.
        try {
            clip.open(is);
        } catch (LineUnavailableException e) {
            System.err.println("No Clip was available.");
            return;
        } catch (IOException e) {
            System.err.println("Error reading the file.");
            return;
        }
    }


    /*
     * Instance Methods
     */

    /**
     * Play the clip and wait until it finishes.
     */
    public synchronized
    void
    play()
    {
        if (clip == null) return;

        clip.start();
        try {wait();} catch (InterruptedException e) {}
    }

}


/**
 * Thread utility methods.
 */
class Utilities {
    /*
     * Class Methods
     */

    /**
     * List all the active thread groups and threads on the console.
     */
    public static
    Thread
    getThread(String name)
    {
        ThreadGroup root;
        for (root = Thread.currentThread().getThreadGroup(); root.getParent() !=
null; root = root.getParent());

        Thread[] threads = new Thread[root.activeCount()];
        int count = root.enumerate(threads);
        for (int iThread = 0; iThread < count; iThread++) {
            Thread thread = threads[iThread];
            if (thread.getName().equals(name)) return thread;
        }

        return null;
    }

    /**
     * List all the active thread groups and threads on the console.
     */
    public static
    void
    listThreads()
    {
        ThreadGroup tgRoot;
        for (tgRoot = Thread.currentThread().getThreadGroup();
tgRoot.getParent() != null; tgRoot = tgRoot.getParent());
        listThreads(tgRoot, 0);
    }

    /**
     * Recursive method that lists all the thread groups and threads in the
given ThreadGroup.
     */
    protected static
    void
    listThreads(final ThreadGroup root, final int nestLevel)
    {
        for (int index = 0; index < nestLevel; index++) System.out.print("  ");

        System.out.println("ThreadGroup: " + root.getName());
        ThreadGroup[] groups = new ThreadGroup[root.activeGroupCount()];
        int count = root.enumerate(groups, false);
        for (int iGroup = 0; iGroup < count; iGroup++) {
            ThreadGroup group = groups[iGroup];
            listThreads(group, nestLevel + 1);
        }

        Thread[] threads = new Thread[root.activeCount()];
        count = root.enumerate(threads, false);
        for (int iThread = 0; iThread < count; iThread++) {
            Thread thread = threads[iThread];
            for (int index = 0; index < nestLevel; index++) System.out.print("  ");
            System.out.println(
                  "  Thread: "
                + (thread.isDaemon()      ? 'D' : ' ')
                + (thread.isAlive()       ? 'A' : ' ')
                + (thread.isInterrupted() ? 'I' : ' ')
                + " " + thread.getName()
            );
        }
    }
}
---------- END SOURCE ----------

CUSTOMER WORKAROUND :
In your console application, find the active non-Daemon "Java Sound event dispatcher" Thread object and call the interrupt() method. The console application will then be able to exit. Note that, since you must depend on the name of the thread, this work around is not sure to be portable
across future versions.
(Review ID: 160615) 
======================================================================

Comments
CONVERTED DATA BugTraq+ Release Management Values COMMIT TO FIX: tiger FIXED IN: tiger INTEGRATED IN: tiger tiger-b14
14-06-2004

EVALUATION ###@###.### 2002-09-04 This is a known problem since J2SE v1.3.0 and I'm surprised that there is no bug filed against it yet. The assumption that this is a memory leak is wrong. The event dispatcher thread is created once for the life time of an application. All per-mixer, and per-line resources are deallocated correctly. The problem is when to release the event dispatcher thread. I see 2 solutions: 1) make it a daemon thread. Then a program will exit, even if sound is still running, upon termination of all other non-daemon threads 2) keep it as a non-daemon thread, but kill it upon finishing all active playback/recording. Can cause confusion because the VM is exiting with a potential delay. Also it introduces some race conditions and may have quite a resource impact (because the event dispatch thread is created and disposed for every sound that is played). Unfortunately, a thread's daemon state cannot be changed... Another problem is that most programs already developed for Java Sound deal with this problem... I think 2) is the better solution, need to discuss this issue with Java Sound users. ###@###.### 2002-12-06 Changed synopsis (old synopsis: "Java Sound resources are never released and can keep a console app from exiting."). Discussion with various Java Sound users and the javasound-interest mailing list showed that changing the thread into a daemon-thread is not compromising compatibility while solving all the problems. Commit to tiger. ###@###.### 2003-08-06 Interesting reading is bug 4365713, of which I wasn't aware of when fixing this bug. The situation has changed, but not much. Applications that use applet.AudioClip and depend on the fact that the virtual machine will not exit will have to exit at a given point, too, so I don't see a compatibility impact. However, there is a problem that applet.AudioClip doesn't allow to query the duration or to query if the sound is still playing. Like that it is impossible to know when to stop sleeping if you want to ensure that the clip is played to the end. For that scenario, I suggest this workaround: Start playing the audio clip. Then, in a loop, check all mixers in Java Sound that provide Source lines, and check if the mixers are open. If there is no open mixer anymore, it can be assumed that the applet Audioclip has finished playback, and the VM can be exited safely. Still better, of course, is using Java Sound's Clip interface, which gives you a listener and isActive() query capability.
11-06-2004

PUBLIC COMMENTS Java Sound keeps a console app from exiting because of non-daemon event thread
10-06-2004