3

This battleship game is the first multi-threaded application I've tried to write and it worked perfectly up until I added the multi-threading,which is only used for sound effects. It is all in one single class, the AudioManager.

I'm pretty sure I just lack experience and or/understanding regarding concurrency, even though I've read the java tutorials etc. I think I just need a little help to get it to click.

Anyway the game runs fine until enough sounds have been played that it runs out of memory and gives me this error:

Exception in thread "AWT-EventQueue-0" java.lang.OutOfMemoryError: unable to create new native thread

I was creating a new thread for each sound effect to play on because I didn't want the gui to wait for the sound to finish, and because sounds are often played very close to each other and I didn't want them conflicting on the same thread if they overlapped. The problem, I think, is that I'm not sure how to close each thread after the sound is played without stalling the main thread.

Here is the class with all the sound code. The sounds are played using the setSound() method, which sets the sound to be played and then starts a new thread with the SoundPlayer inner class for the Runnable. Thanks in advance:

import java.io.IOException;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;

public class AudioManager {

    private static Thread backgroundThread = new Thread();
    private static int loopCounter = 2;
    private static Clip clip;
    private static String[] backgroundFiles = {
        "/40_Avalon.wav","/13_Glatisant.wav",
        "/31_Lying_In_Deceit.wav","/43_Return_to_Base.wav"};
    private static String[] files = {
        "/bigboom.wav","/Robot_blip.wav",
        "/battleStations.WAV","/beep1.wav",
        "/button-47.wav","/button-35.wav",
        "/beep-23.wav","/Sonar_pings.wav",
        "/button-21.wav","/SONAR.WAV"};
    private static AudioInputStream currentBackgroundMusic;
    private static AudioInputStream currentSound;
    private static boolean backgroundOn = false;
    private static boolean canStart = true;


    private static AudioInputStream loadSound(int s){

        AudioInputStream stream = null;

        try {
            stream = AudioSystem.getAudioInputStream(AudioManager.class.getClass().getResource(files[s]));
        } catch (UnsupportedAudioFileException | IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return stream;

    }

    private static AudioInputStream loadBackground(int s){

        AudioInputStream stream = null;

        try {
            stream = AudioSystem.getAudioInputStream(AudioManager.class.getClass().getResource(backgroundFiles[s]));
        } catch (UnsupportedAudioFileException | IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return stream;

    }


    public static void setSound(int s){
        currentSound = loadSound(s);
        Thread thread = new Thread(new SoundPlayer());
        thread.start();
    }

    private static void continueMusic(){
        setBackgroundMusic(loopCounter);
        loopCounter++;
        if(loopCounter > 3) loopCounter = 0;

    }




    public static void playSound(){
        try {
            clip = AudioSystem.getClip();
            clip.open(currentSound);
        } catch (LineUnavailableException | IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        clip.start();
    }


    public static void setBackgroundMusic(int s){
        if (backgroundOn) {
            backgroundOn = false;
            canStart = false;

            try {
                backgroundThread.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

        currentBackgroundMusic = loadBackground(s);
        backgroundThread = new Thread(new MusicPlayer());
        backgroundOn = true;
        backgroundThread.start();
        canStart = true;
    }

    private static void playSound2(AudioInputStream audio) {

        AudioFormat     audioFormat = audio.getFormat();
        SourceDataLine  line = null;
        DataLine.Info   info = new DataLine.Info(SourceDataLine.class,audioFormat);

        try{
            line = (SourceDataLine) AudioSystem.getLine(info);
            line.open(audioFormat);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        line.start();

        int     nBytesRead = 0;
        byte[]  abData = new byte[128000];

        while (nBytesRead != -1 && backgroundOn)
        {
            try{
                nBytesRead = audio.read(abData, 0, abData.length);
            } catch (IOException e){
                e.printStackTrace();
            }

            if (nBytesRead == -1) break;

            line.write(abData, 0, nBytesRead);

        }

        line.drain();
        line.stop();
        line.close();
        line = null;
        backgroundOn = false;
    }

    private static class MusicPlayer implements Runnable{

        @Override
        public void run() {
            playSound2(currentBackgroundMusic); 
        }
    }

    private static class SoundPlayer implements Runnable{

        @Override
        public void run() {
            playSound();

        }
    }

    public static void loopMusic(){
        Thread loop = new Thread(new Runnable(){

            @Override
            public void run() {
                while(true){
                    if(backgroundThread.isAlive()){
                        try {
                            backgroundThread.join(0);
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    } else if (canStart){
                        continueMusic();
                    }
                }

            }});

        loop.start();
    }

    public static void reset(){
        loopCounter = 2;
    }


}
zephos2014
  • 331
  • 1
  • 2
  • 13
  • "unable to create new native thread" <-- this probably means you lack address space; do you use a 32bit JVM by any chance? – fge Nov 30 '14 at 00:18

5 Answers5

1

First of all, thank you to everyone who posted answers. You all helped a lot and the solution was a combination of your answers. I've decided to post my own answer with the solution I came up with for the benefit of others who may have the same issue.

It turns out, I was indeed creating too many threads and the OS only lets Java have a certain amount of memory space. So I fixed that by using an ExecutorService.

However, I was still having the same problem, even though I wasn't explicitly creating lots of new threads. Why? because I was creating new Clips to play sounds.

I think the Clips are somehow creating threads to play sounds on, so they can play without locking up the program or GUI (which I didn't understand before). So, to solve the problem once and for all, and also to allow my game to play the exact same sound rapidly in succession without clipping or having to wait for the previous sound to finish, I got rid of the executor and created ten Clips for each sound and that's all.

When a sound is played, it increments an index so that the next time that sound is played, it will actually use a different clip (but loaded with the same exact sound) and it prepares the next clip to play too.

My game not longer creates excessive threads or clips and runs great! The updated code is below, along with a couple of tests that I used to find out what was going on:

import java.io.IOException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineListener;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;

public class AudioManager {

    private static ExecutorService backgroundPool = Executors.newFixedThreadPool(1);
    private static Future<?> backgroundStatus;
    private static int loopCounter = 2;
    private static String[] backgroundFiles = {
        "/40_Avalon.wav","/13_Glatisant.wav",
        "/31_Lying_In_Deceit.wav","/43_Return_to_Base.wav"};
    private static String[] files = {
        "/bigboom.wav","/Robot_blip.wav",
        "/battleStations.WAV","/beep1.wav",
        "/button-47.wav","/button-35.wav",
        "/beep-23.wav","/Sonar_pings.wav",
        "/button-21.wav","/SONAR.WAV"};
    private static AudioInputStream currentBackgroundMusic;
    private static boolean backgroundOn = false;
    private static boolean canStart = true;

    private static int[] clipIndex = new int[10];

    private static Clip[][] clips = new Clip[10][10];

    private static void initializeClips(int sound){

        clipIndex[sound] = 0;

        for (int i = 0 ; i < 10 ; i++)
            try {
                clips[sound][i] = AudioSystem.getClip();
                clips[sound][i].open(loadSound(sound));
                clips[sound][i].addLineListener(new LineListener(){

                    @Override
                    public void update(LineEvent event) {
                        if(event.getType() == javax.sound.sampled.LineEvent.Type.STOP){
                            clips[sound][clipIndex[sound]].setFramePosition(0);
                        }
                    }});
            } catch (LineUnavailableException | IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
    }





    private static AudioInputStream loadSound(int s){

        AudioInputStream stream = null;

        try {
            stream = AudioSystem.getAudioInputStream(AudioManager.class.getClass().getResource(files[s]));
        } catch (UnsupportedAudioFileException | IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return stream;

    }

    private static AudioInputStream loadBackground(int s){

        AudioInputStream stream = null;

        try {
            stream = AudioSystem.getAudioInputStream(AudioManager.class.getClass().getResource(backgroundFiles[s]));
        } catch (UnsupportedAudioFileException | IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return stream;

    }


    public static void setSound(int s){
        if(clips[s] == null){
            initializeClips(s);
        }
        playSound(s);
    }

    private static void continueMusic(){
        setBackgroundMusic(loopCounter);
        loopCounter++;
        if(loopCounter > 3) loopCounter = 0;
    }

    public static void playSound(int sound){
        if(clips[sound][0] == null){
            initializeClips(sound);
        }
        clips[sound][clipIndex[sound]].start();
        clipIndex[sound]++;
        if(clipIndex[sound] == 10){
            clipIndex[sound] = 0;
        }
        clips[sound][clipIndex[sound]].drain();
        clips[sound][clipIndex[sound]].flush();
        clips[sound][clipIndex[sound]].setFramePosition(0);

    }


    public static void setBackgroundMusic(int s){

        canStart = false;

        if (backgroundOn) {
            backgroundOn = false;
        }
        currentBackgroundMusic = loadBackground(s);
        backgroundStatus = backgroundPool.submit(new MusicPlayer());
        canStart = true;
    }

    private static void playSound2(AudioInputStream audio) {

        backgroundOn = true;
        AudioFormat     audioFormat = audio.getFormat();
        SourceDataLine  line = null;
        DataLine.Info   info = new DataLine.Info(SourceDataLine.class,audioFormat);

        try{
            line = (SourceDataLine) AudioSystem.getLine(info);
            line.open(audioFormat);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        line.start();

        int     nBytesRead = 0;
        byte[]  abData = new byte[128000];

        while (nBytesRead != -1 && backgroundOn){
            try{
                nBytesRead = audio.read(abData, 0, abData.length);
            } catch (IOException e){
                e.printStackTrace();
            }

            if (nBytesRead == -1) break;

            line.write(abData, 0, nBytesRead);

        }

        line.drain();
        line.stop();
        line.close();
        line = null;
        backgroundOn = false;
    }

    private static class MusicPlayer implements Runnable{

        @Override
        public void run() {
            playSound2(currentBackgroundMusic); 
        }
    }

    public static void loopMusic(){

        Thread loop = new Thread(new Runnable(){

            @Override
            public void run() {
                while(true){
                    if(backgroundStatus.isDone() && canStart){
                        continueMusic();
                    }
                }

            }});

        loop.start();
    }
    public static void reset(){
        loopCounter = 2;
    }


}

The following is a test that will tell you how many threads your operating system lets the JVM create. Once you get the error, just look at the last number that was printed to the console.

public class Test1 {

    static long count = 0L;

    public static void main(String[] args) {
        while(true){
            count ++;
            System.out.println(count);
            new Thread(new Runnable(){

                @Override
                public void run() {
                    try {
                        Thread.sleep(60000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }

                }}).start();
        }

    }

}

And the following is a test that does the same thing, except by creating clips and opening resources. Notice that the clips themselves don't require a thread, but once you open them they do. You should get the same number (or close) before the error with each test. Of course, you will have to provide your own sound file to run the second one.

import java.io.IOException;
import java.util.ArrayList;

import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;

import audio.AudioManager;

public class Test2 {

    static long count = 0L;
    static ArrayList<Clip> clips = new ArrayList<>();

    public static void main(String[] args) {
        while(true){
            count ++;
            System.out.println(count);
            try {
                Clip clip1 = AudioSystem.getClip();
                clip1.open(loadSound());
                clips.add(clip1);

            } catch (LineUnavailableException | IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

        }

    }

    public static AudioInputStream loadSound(){

        AudioInputStream stream = null;

        try {
            stream = AudioSystem.getAudioInputStream(AudioManager.class.getClass().getResource("/bigboom.wav"));
        } catch (UnsupportedAudioFileException | IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return stream;

    }

}
zephos2014
  • 331
  • 1
  • 2
  • 13
  • You are right that the Clip itself creates a native thread. That's why you eventually exhaust native threads even if you use an ExecutorService to run the clips. –  Dec 30 '15 at 02:43
0

Well, as you said, your problem is that your threads are still either running or they stopped but the memory they used is not being released by java's garbage collector (GC).

A thread stops when their run() method returns (finishes) or throws an exception. If that happens, and there is NO REFERENCES to that thread anywhere in your code, it will be deleted by the GC eventually (if your program needs some memory, for example).

So, first, check that those threads you create aren't in an nasty infinite loop like the one below:

public void run() {
    while(true){
        //...
        //Some code
        //...
    }
}

and once you are sure they are ending propertly after playing the sound, make sure they're not being referenced anywhere else in your code (a part of your program still maintaining a pointer to that object).

One last note. Consider checking the Thread Pool Pattern for this kind of things. I prefer it over just creating a new thread for each new task. It can be less messy and more efficient.

Alfro
  • 458
  • 9
  • 22
0

You need to get rid of all of these static variables. This code is not thread safe, as you are effectively trying to use all of the static variables as a shared state.

I would recommend that you start passing state around your runnable objects. If they need to communicate with each other, use the built in concurrency utilities that come with Java. If that is not sufficient, use synchronization and mutate object state.

After a cursory look at your code I can tell that your code will suffer from memory visibility issues. You could try to fix this by making your static variables volatile, but I doubt that it will be sufficient. It would make much cleaner code if you encapsulate the state within individual objects.

Before you do anything further, I would step back and try to spend 10-15 minutes and come up with an overall design. What objects will you have, what responsibilities will each one have and what state will they have (immutable/mutable). How will those objects synchronize with each other (if they run in different threads?

Nick Hristov
  • 905
  • 1
  • 8
  • 17
  • By passing a state through the Runnable objects, are you talking about the variable that indicates which sound is to be played, and do you mean passing them as a parameter in the constructor? – zephos2014 Nov 30 '14 at 17:21
0

Your code is not thread safe.

Wait, let me get to that later. Your OOME is being caused by too many threads being created. Your current experience with concurrency is just reading online tutorials, right? Those only cover how to synchronize and share, and not how threads work.

Threads have a SIGNIFICANT setup and teardown overhead, and what you're doing is called unbounded thread creation, meaning you just create more and more threads until the system can't support it anymore.

First off, you can fix your problem using thread pooling, or more specifically, an ExecutorService to execute tasks concurrently. A cached thread pool is all you need.

Secondly, you have a ton of shared state fields. You can fix this by making an immutable wrapper for a single state snapshot every operation on an AtomicReference (or something of the like), or you can synchronize access to your fields.

Third, please get rid of all of your static fields and methods. I don't see it as appropriate in this case, although there's not enough code to validate my concern.

  • Thank you for your advice. Is there something inherently wrong with using static fields and methods that I'm not aware of? I do that a lot when there's no real need to create an object just to do utilitarian tasks, but I've only been programming for less than five months so I'm still learning. – zephos2014 Nov 30 '14 at 17:26
  • Another question: I've been looking into the ExecutorService and I'm not sure how to shut it down after the program is closed. I'm using a JFrame so the closing operations are handled in the background. How would I take that over to shut the thread pool down? – zephos2014 Nov 30 '14 at 17:42
  • I primarily thing it's a design issue, but no matter. Remember that Java is OOP. In an `ExecutorService`, you are referring to the `shutdown` method, correct? –  Nov 30 '14 at 20:30
  • Yes. How can I make sure shutDown method gets called when someone clicks the X button on a JFrame, if I set the JFrame's close operations to EXIT_ON_CLOSE ? From what I've read, it won't be called automatically and the JVM will keep running, unlike other processes. – zephos2014 Nov 30 '14 at 23:42
  • Hey, I implemented the ExecutorService as you suggested, however it throws the same error after enough sounds have played. I even output the number of threads the threadPool has every time a sound is played, and it never goes above the minimum which I set in the threadPool's constructor. If no new threads are needed, what could be causing the OutofMemory error? Thanks. – zephos2014 Dec 01 '14 at 02:14
  • And what might be your maximum threads be? Try adjusting it around 1-4 threads. –  Dec 01 '14 at 05:43
  • Maybe I had it too high... around 10. But I discovered the real problem... I guess when I create Clips, they automatically ask for a new thread to run on, and that's what was reaching the limit even after I used a ThreadPool. Thanks for your help though! – zephos2014 Dec 01 '14 at 05:45
0

"Clip.start() method returns immediately, and the system playbacks the sound file in a background thread." (from this question which discusses how to play sounds after each other).

Since the threads you create effectively run "make clip object and start it", they actually do hardly anything. Even the I/O operations (opening the stream) are done at forehand (in the main GUI thread).

Your assumption that the GUI has to wait for a clip to finish does not appear to be valid. And I doubt they can be conflicting on the same thread if they overlap. Can you confirm the GUI is more responsive with multi-threading? My guess is that it is actually less responsive since creating and starting new threads is not cheap.

Community
  • 1
  • 1
vanOekel
  • 6,358
  • 1
  • 21
  • 56
  • 1
    Yes I can confirm that. I tried playing the sounds on the main thread and they would clip each other, and sometimes would play intermittently. The GUI was slow too. When I put them on other threads it worked fines... except for the running out of space of course. – zephos2014 Nov 30 '14 at 17:28
  • @zephos2014 Hmm, even [this answer](http://stackoverflow.com/a/23487878/3080094) seems to confirm it should not interfere with the main GUI thread. But I do not know what is causing the delays in the main GUI thread, or why multi-threading helps. So my answer is not helping, I'll delete it shortly. – vanOekel Nov 30 '14 at 17:51
  • No need to delete it. It is still helpful as a means of narrowing down possibilities to find the correct solution. Thank you! – zephos2014 Nov 30 '14 at 23:43
  • 1
    Hey you ended up being right! I got it working without the use of extra threads. I think Clips run concurrently anyway, and the problem was that I was creating too many clips, thus creating too many threads behind the scenes! – zephos2014 Dec 01 '14 at 05:43