Java Solaris Communities Sun Store Join SDN My Profile Why Join?
 
Bug Database
Bug Detail
Quick Lists
Top 25 Bugs
Top 25 RFE's
Recently Closed Bugs
Printable Page Printable Page


Bug Database
Bug ID: 4735740
Votes 5
Synopsis Java Sound keeps a console app from exiting because of non-daemon event thread
Category java:classes_sound
Reported Against 1.3 , hopper-rc , mantis-beta
Release Fixed 1.5(tiger)
State 10-Fix Delivered, bug
Priority: 4-Low
Related Bugs 4365713 , 4380283 , 4435394 , 4785530 , 4872850 , 5070730
Submit Date 22-AUG-2002
Description




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 :
 customer  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  customer  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) 
======================================================================
Work Around
N/A
Evaluation
  xxxxx@xxxxx   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.


  xxxxx@xxxxx   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.


  xxxxx@xxxxx   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.
Comments
  
  Include a link with my name & email   

Submitted On 24-APR-2004
qwecxvb
in javasound-interest archives Jörg Prante  provides a
simple work-around call System.exit(0)  in your main() function.
see
http://archives.java.sun.com/cgi-bin/wa?A2=ind0002&L=javasound-interest&F=&S=&P=12213
for details


Submitted On 04-AUG-2004
jpyeron
more test cases patch, wanted to verify that an interrupted eventdispatcher could work again.

--- PlaySound.java	2002-08-22 00:00:00.000000000 -0400
+++ PlaySound.java	2004-08-04 09:54:01.816219200 -0400
@@ -13,6 +13,13 @@
      * Class Methods
      */
 
+    public static void main(String args[]) throws Throwable
+    {
+        main2(args);
+        System.out.println("Sleeping 7 seconds");
+        Thread.sleep(7000);
+        main2(args);
+    }
 
     /*
      * Plays the given sound file.
@@ -21,8 +28,11 @@
      */
     public static
     void
-    main(String[] args)
+    main2(String[] args)
     {
+        System.out.println("Java Vendor :"+System.getProperty("java.vendor"));
+        System.out.println("Java Version:"+System.getProperty("java.version"));
+
         // Parse arguments.
         final int argCount = args.length;
         if (argCount == 0 || argCount > 2) {
@@ -62,6 +72,8 @@
         System.out.println("Done.");
         if (interruptThread) {
             Thread thread = Utilities.getThread("Java Sound event dispatcher");
+            System.out.print("Interrupting: ");
+            Utilities.printThread(thread);
             if (thread != null) thread.interrupt();
         }
         Utilities.listThreads();
@@ -198,6 +210,22 @@
     }
 
     /**
+     * Prints the detail for a given thread.
+     */
+    public static 
+    void 
+    printThread(Thread thread)
+    {
+        System.out.println(
+              "  Thread: "
+            + (thread.isDaemon()      ? 'D' : ' ')
+            + (thread.isAlive()       ? 'A' : ' ')
+            + (thread.isInterrupted() ? 'I' : ' ')
+            + " " + thread.getName()
+        );
+    }
+
+    /**
      * Recursive method that lists all the thread groups and threads in the
 given ThreadGroup.
      */
@@ -220,13 +248,7 @@
         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()
-            );
+            printThread(thread);
         }
     }
 }


Submitted On 04-AUG-2004
jpyeron
Still broken, reopen, using test program 

C:\tmp>c:\cygwin\bin\date.exe && java PlaySound TADA.WAV
Wed Aug  4 09:16:35 EDT 2004
Java Vendor :Sun Microsystems Inc.
Java Version:1.4.2_05
File: TADA.WAV  Frame Length: 42752  Format: PCM_SIGNED
Clip: interface Clip supporting 8 audio formats, and buffers of 0 to 4194304 bytes
*** Line Event: Open ***
*** Line Event: Start ***
*** Line Event: Stop ***
*** Line Event: Close ***
Done.
ThreadGroup: system
  ThreadGroup: main
    Thread:  A  main
    Thread:  A  Java Sound event dispatcher
    Thread: DA  Headspace mixer frame proc thread
  Thread: DA  Reference Handler
  Thread: DA  Finalizer
  Thread: DA  Signal Dispatcher
  Thread: DA  CompilerThread0


Submitted On 04-AUG-2004
jpyeron
I have made a patch to test some more:
see notes in bug 4365713
--- PlaySound.java	2002-08-22 00:00:00.000000000 -0400
+++ PlaySound.java	2004-08-04 09:40:21.066049600 -0400
@@ -13,6 +13,13 @@
      * Class Methods
      */
 
+    public static void main(String args[]) throws Throwable
+    {
+        main2(args);
+        System.out.println("Sleeping 7 seconds");
+        Thread.sleep(7000);
+        main2(args);
+    }
 
     /*
      * Plays the given sound file.
@@ -21,8 +28,11 @@
      */
     public static
     void
-    main(String[] args)
+    main2(String[] args)
     {
+        System.out.println("Java Vendor :"+System.getProperty("java.vendor"));
+        System.out.println("Java Version:"+System.getProperty("java.version"));
+
         // Parse arguments.
         final int argCount = args.length;
         if (argCount == 0 || argCount > 2) {



PLEASE NOTE: JDK6 is formerly known as Project Mustang