title: Java线程池实现音频播放 date: 2019-05-06 21:00:00
Java线程池实现音频播放
参考文章:
Java并发编程:线程池的使用
必须要理清的Java线程池
关于什么是多线程?什么是线程池可以看看参考文章,我觉得大佬们描述的,比较全面吧,反正我不太想码字......下面我写一下我为啥要用Java线程池实现音乐播放吧,先说一下背景:我需要在一个特定的条件,例如你触动鼠标或按键时,播放音频。这个就是我想实现的效果,下面我贴上我一开始用线程实现的代码
package com.gzcodestudio.soundeffects.event; import sun.audio.AudioPlayer; import sun.audio.AudioStream; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; /** * @author hkq * @date 2019/05/01 */ public class FirstMusic extends Thread { private InputStream url = null; private AudioStream audioStream = null; private boolean[] on_off = null; public FirstMusic(boolean[] on_off) { this.on_off = on_off; } @Override public void run() { AudioPlayer.player.stop ( audioStream ); try { url = new FileInputStream ( FirstMusic.class.getClassLoader ( ).getResource ( "a1.wav" ).getPath ( ) ); // 创建音频流对象 audioStream = new AudioStream ( url ); // 使用音频播放器播放声音 AudioPlayer.player.start ( audioStream ); } catch (IOException e) { e.printStackTrace ( ); } finally { //如果为空,关闭资源 try { if (url == null) { url.close ( ); } if (audioStream == null) { audioStream.close ( ); } } catch (IOException e) { e.printStackTrace ( ); } } } }
代码比较蹧啊,单纯的写了一个线程实现音频播放,这个AudioStream类就是实现音频播放的音频流,调用的时候需要创建一个新的音频流对象接收它......这个线程的调用方式就是在其他监听条件里创建一个新的线程对象接收它,并运行它的strat()方法。代码如下:
//开启音频播放线程 boolean[] on_off = {true}; FirstMusic music = new FirstMusic ( on_off ); music.start ( );
效果是达到我所想的效果了,但是我总觉得它是在每一次监听条件里创建一个新的线程对象运行这个线程...这样子如果只是运行一次,问题就不大,但如果我是做了鼠标监听,玩个枪战游戏,不停的点击鼠标,那它会创建多少线程对象啊......资源就浪费掉了,严重会造成电脑死机。当我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
在Java中可以通过线程池来达到这样的效果。下面我写一下我的实现过程吧。首先你得认真看看文章推荐的文章参考,理解一下java中的ThreadPoolExecutor类,java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,因此如果要透彻地了解Java中的线程池,必须先了解这个类。看完这个类诸多的方法了吗?晕吗?还是想试试码码代码了?对不起,本次需要用到的是ThreadFactory,ScheduledThreadPoolExecutor...
ThreadFactory是一个线程工厂。用来创建线程。这里为什么要使用线程工厂呢?其实就是为了统一在创建线程时设置一些参数,如:是否守护线程。线程一些特性等,如优先级。通过这个TreadFactory创建出来的线程能保证有相同的特性。它首先是一个接口类,而且方法只有一个。就是创建一个线程。
ScheduledThreadPoolExecutor继承ThreadPoolExecutor来重用线程池的功能,它的实现方式如下:
将任务封装成ScheduledFutureTask对象,ScheduledFutureTask基于相对时间,不受系统时间的改变所影响;
ScheduledFutureTask实现了java.lang.Comparable
接口和java.util.concurrent.Delayed
接口,所以有两个重要的方法:compareTo和getDelay。compareTo方法用于比较任务之间的优先级关系,如果距离下次执行的时间间隔较短,则优先级高;getDelay方法用于返回距离下次任务执行时间的时间间隔;
ScheduledThreadPoolExecutor定义了一个DelayedWorkQueue,它是一个有序队列,会通过每个任务按照距离下次执行时间间隔的大小来排序;
ScheduledFutureTask继承自FutureTask,可以通过返回Future对象来获取执行的结果。
ScheduledThreadPoolExecutor可以用来在给定延时后执行异步任务或者周期性执行任务,相对于任务调度的Timer来说,其功能更加强大,Timer只能使用一个后台线程执行任务,而ScheduledThreadPoolExecutor则可以通过构造函数来指定后台线程的个数。
懂了吗?你百度吧,我也说不清楚!可以参考一下以下文章
线程池创建
线程池之ScheduledThreadPoolExecutor
ThreadFactory工厂模式
我看完上述文章,还是一知半解的情况下啊!贴上以下代码:
创建线程池:
package com.gzcodestudio.soundeffects.util; import org.apache.commons.lang3.concurrent.BasicThreadFactory; import java.util.concurrent.ScheduledThreadPoolExecutor; /** * @author DHB */ public abstract class ThreadUtil { /** * 创建线程池 * * @param corePoolSize 线程池大小 * @return 线程池 */ public static ScheduledThreadPoolExecutor newExecutorService(int corePoolSize, String name) { return new ScheduledThreadPoolExecutor(corePoolSize, new BasicThreadFactory.Builder().namingPattern(name + "-%d").daemon(true).build()); } }
只解析一下new BasicThreadFactory.Builder().namingPattern(name + "-%d").daemon(true).build());这行代码,Factory结尾的是用了工厂模式,工厂模式就是用来生成对象的,相当于一个构造器,内部会帮你构造出对象来,Builder又是建造者模式。线程池创建出来了,下面代码是我使用线程池实现音频播放的FirstMusicThread类的代码:
FirstMusicThread:
package com.gzcodestudio.soundeffects.event; import com.gzcodestudio.soundeffects.util.ThreadUtil; import sun.audio.AudioPlayer; import sun.audio.AudioStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.concurrent.ScheduledThreadPoolExecutor; /** * 音效播放监听 * * @author hkq */ public class FirstMusicThread implements MusicListener { /** * 停止任务 */ public static final int STOP = 0; /** * 任务运行中 */ public static final int RUNNING = 1; /** * 重新运行 */ public static final int RESUME = 2; private ScheduledThreadPoolExecutor poolExecutor; private volatile int resume = RESUME; private volatile boolean isRun = true; private Runnable runnable; private InputStream url; private AudioStream audioStream; private Object callback; /** * 初始化 */ @Override public void init() { url = null; audioStream = null; poolExecutor = ThreadUtil.newExecutorService ( 1 , this.getClass ( ).getName ( ) ); } /** * 监听 */ @Override public void listening() { if (runnable == null) { newTask ( ); poolExecutor.submit ( runnable ); } else { throw new IllegalArgumentException ( "listening() 仅允许执行一次" ); } } /** * 监听 * * @param callbacks 回调 */ @Override public void callback(Object callback) { this.callback = callback; } /** * 创建播放线程 */ public void newTask() { runnable = () -> { while (isRun) { if (resume == RESUME) { // 播放 try { url = new FileInputStream ( FirstMusicThread.class.getClassLoader ( ).getResource ( "a1.wav" ).getPath ( ) ); // 创建音频流对象 audioStream = new AudioStream ( url ); // 使用音频播放器播放声音 AudioPlayer.player.start ( audioStream ); } catch (FileNotFoundException e) { e.printStackTrace ( ); } catch (IOException e) { e.printStackTrace ( ); } finally { //如果资源为空,关闭资源 try { if (url == null) { url.close ( ); } if (audioStream == null) { audioStream.close ( ); } } catch (IOException e) { e.printStackTrace ( ); } } // 复位 resume = RUNNING; } if (resume == RUNNING) { /* * 处理音频事件 * https://stackoverflow.com/questions/10684631/key-listener-written-in-java-jna-cannot-stop-the-thread * PeekMessage 非阻塞 * GetMessage 阻塞 * */ } } }; } /** * 取消监听 */ @Override public void unListening() { resume = STOP; } /** * 恢复监听 */ @Override public void resume() { resume = RESUME; } /** * 结束 */ @Override public void destroy() { isRun = false; unListening ( ); poolExecutor.shutdown ( ); } }
上述代码是我复用线程池写的代码,代码里我用上了一些接口,目的是更好地了解当前音频播放的状态,所以我下面就是我的音频播放监听接口的代码,新创建一个接口类MusicListener
MusicListener:
package com.gzcodestudio.soundeffects.event; /** * 音效监听接口 * @author hkq */ public interface MusicListener{ /** * 初始化 */ void init(); /** * 监听 */ void listening(); /** * 监听 * * @param callback 回调 */ void callback(T callback); /** * 取消监听 */ void unListening(); /** * 恢复监听 */ void resume(); /** * 结束 */ void destroy(); }
下面就调用复用线程池类FirstMusicThread的方法实现音频播放效果,本来我是打算使用以下代码调用的,结果发现不行啊,算了,代码贴上来,给大佬们看看
Test:
package com.gzcodestudio.soundeffects.event; import org.junit.After; import org.junit.Before; import org.junit.Test; @SuppressWarnings("all") public class Test { private FirstMusicThread firstMusicThread; @Before public void before() { firstMusicThread = new FirstMusicThread ( ); } @Test public void listening() { firstMusicThread.init ( ); firstMusicThread.callback(types,callback->{ switch(types){ case FirstMusicThread.RUNNING: //开启音频播放 firstMusicThread.unListening(); break; case FirstMusicThread.STOP: firstMusicThread.resume(); break; default: } }); firstMusicThread.listening ( ); new Thread ( () -> { while (true) { try { Thread.sleep ( 5000 ); } catch (InterruptedException e) { e.printStackTrace ( ); } firstMusicThread.resume ( ); System.out.println ( "测试自动恢复" ); } } ).start ( ); while (true) { } } @After public void after() { firstMusicThread.unListening ( ); firstMusicThread.destroy ( ); } }
使用上面Test类这个代码调用音频播放就要再创建一个MusicCallback接口类,实现监听回调,代码如下:
MusicCallback:
package com.gzcodestudio.soundeffects.event; /** * 音效监听回调 * * @author dhb */ @FunctionalInterface public interface MusicCallback{ /** * 回调 * * @param type 键盘事件类型 * @param t 类型 */ void callback(int types, T t); }
我断点调了老半天,还是没能解决这个问题,所以我就没有用那个音效监听回调的MusicCallback类,而是直接在测试类里调用,效果达到我的预期效果,直接在需要使用的位置添加:
//取消监听 firstMusicThread.unListening(); //开启音频播放监听 firstMusicThread.listening(); //恢复音频播放监听 firstMusicThread.resume(); //结束监听 firstMusicThread.destroy();
学无止境啊!!!