vlambda博客
学习文章列表

Java游戏编程不完全详解-1


前言


1991年,我第一次在DOS操作系统下玩“F-117A Stealth Fighter 2.0 ”游戏,这是一款像素级的模拟器游戏。

Java游戏编程不完全详解-1

Java游戏编程不完全详解-1

Java游戏编程不完全详解-1

Java游戏编程不完全详解-1

Java游戏编程不完全详解-1

Java游戏编程不完全详解-1

Java游戏编程不完全详解-1


于是,我这辈子被种草游戏了--从此爱上了游戏,并且想写一款游戏。于是,我考大学时就报考了计算机系专业,因为别人告诉我大学里会学怎么编程啊、肯定也会编写游戏啊等等...


不过,呵呵,相信大家也知道这是一个谎言!我上了大学后发现别人告诉我的东西根本没有不存在,差一点被害得在毕业时都入不了IT行业,还谈什么游戏开发了Java游戏编程不完全详解-1。结果搞了10年的EPR应用开发--因为得先填饱自己肚子实现了生存再说哦Java游戏编程不完全详解-1


不过,写游戏的梦想一直存在我的心中,直到2011年我转行做了手机游戏的开发,本大黍是第一批使用Coco2d-x开源引擎做手机游戏开发的前辈--第一款是使用0.91版本开发的,直到现在使用的cocos2d-x 4.0版本Java游戏编程不完全详解-1


关于C++游戏的话题,我们以后再说。如果大家感兴趣C++游戏开发的话,请耐心等待我们未来出C++系列的文章,请喜欢C++游戏开发的小伙伴们关注+点赞+收藏+评论+转发哦Java游戏编程不完全详解-1~


好了,言归正传,我们下面解读游戏开发技术,作为计科系出身的程序员我的翻译只能做一个参考,请大家不要当成标准。如果有不足之处,请大家指正和补充。


Java游戏编程之多线程


对于游戏用来说,对游戏第一个的要求就是运行高效--运行一定要流畅,画面一定要美!为保证游戏运行高效和流畅,因此我们必须从游戏的技术选型上就必须要考虑,再加上现代硬件非常高效,操作系统都是多进程和多线程的,因此怎样利用多线程来让程序变得更加高效,这是一个必选题。


当然,我们可以参见“Java多线程第2版不完全详解”一篇文章,以帮助咱们从多角度来理解Java多线程。因为不同的专业书籍对Java多线程的认知是不一样的。呵呵,大家是不是觉得这个说法有一点搞笑,对吧?这个问题在C/C++那里,那就不是一个问题。由于Java虚拟机是不开源的,所以大家都可以各抒己见,百家争鸣。


为什么使用Java编游戏?


Java 1.4版本以后,我们可使用Java平台来开发快速的、全屏幕的和硬件加速(显卡)的游戏!同时,使用Java意味着可以使用复杂的API来简化OOP编程、简化的多线程编程、自动的垃圾回收 ,以及良好的可移植性。除些之外,还有大量开源的库以及优雅、方便的IDE等来使用。


Java相对于C和C++就是它的速度问题,但是如果使用HotSpot VM和独立显卡之后,那么它的游戏运行速度就不是问题了。HotSpot技术是把游戏在运行时编译到本地码中去,加上强大的独立显卡,这时Java编写的游戏就不再会有运行速度的困扰。


什么是多线程?


如果把计算机处理器看成是一个熟练的侍者,而把用户看成是一个任务,那么每个任务都有自己的线程(Thread)。而一个处理器在现代操作中可以并发(concurrently)运行多个线程。比如,我们时常会从互联上下载电影时,还听着音乐的写着代码(^_^)…或者边聊QQ边写代码等。


现代的操作系统并发运行线程时,是把线程的任务分解成更小的块(单元)来处理的—这就叫做并发(concurrency)。一个线程只有一小块时间片来执行,然后该线程被悬空(pre-empted),以便让其它的线程运行,然后如此循环。如示下图:

Java游戏编程不完全详解-1

  • hread A--线程A

  • Thread B--线程B

  • Thread A Starts--线程A启动

  • THread B Starts--线程B启动


使用Java创建线程和使用线程


其实Java就是使用线程概念被设计的,所以我们会发现在Java使用线程工作是非常容易的事情,如果想创建并且启动一个新的线程,那么我们只需要创建一个Thread对象的实例,然后呼叫它的start()方法即可。


Thread myThread = new Thread();myThread.start();

当然该代码没有做任何事情,因为JVM只是创建一个新的系统线程(system thread),然后启动了它,最后呼叫了该线程对象的run()方法,但是run方法没有做任何事情。

使用线程最便捷的方式是直接继承Thread类,然后重写run方法:

public class MyThread extends Thread{ public void run(){ System.out.println(“do something”); }}

然后创建这个类的对象,然后启动它:

Thread myThread = new MyThread();myThread.start();

现在我们两个线程在运行了:主线程和我们现在创建的子线程对象。

继承Thread类非常容易,但是大多数时候我们可能不希望书写一个新的类型就想启动一个线程。比如,我们希望继承另外一个类,但是又想把段代码作为一个线程来运行,那么这种情况下我们需要实现Runnable接口:


public class MyClass extends BaseDAO implements Runnable{ public MyClass(){ Thread thread = new Thread(this); thread.start(); } //实现run方法 public void run(){ System.out.println(“Do something cool here!”); }}

以上示例MyClass对象在构造方法启动了一个新的线程。

Thread类把Runnable对象作为它的构造方法的参数,而Runnable对象是在该线程被启动时执行。有时候我们不想新创建一个类,但是又想封装一个线程,这时时候我们可以使用匿名内部类来实现:

new Thread(){ public void run(){ System.out.println(“Do something cool here”); }}.start();


该示例代码非常简单,但是如果run方法的代码太长,那么它的可读性就非常差(解读:这种写法在现代叫做流式布局写法,非常的流行,但是在Java之初不是赞成的。这个Java之初的时间,我认为应该是Java在没有被卖给Oracle之前,它一直强调严谨的风格,因为我是JDK 1.4版本的程序员,曾经考过Sun公司的Java认证嘛Java游戏编程不完全详解-1,所以咱们是非常了解什么是最正宗的Java代码风格的。本人并不认为Java 5以后的版本是真正的Java了,它已经变得四不像了,特别是当Java 8引入了函数编程-lambda表达式之后。当然,这个仅代表本人的观点,不喜勿喷哈Java游戏编程不完全详解-1)。


如果我们需要我们当前的线程等待另外一个线程运行完成,那么使用join()方法:

myThread.join();

该方法非常有用,它一般使用来让一个玩家退出我们的游戏,因为我们需要等待所有线程都运行完成,然后才能做复位的动作。如果我们需要让线程休息一下,比如让一个线程暂停一会儿,那么使用sleep()方法:

myThread.sleep(1000);

这样做结果是让当前运行的线程睡觉一秒钟,但是睡觉不会CPU的时间—当然它不会做梦的。


线程同步


很好,现在我们可以使用多个线程来同时做一些非常cool的事情了,但是这并不表示万事大吉了。因为,如果出现多个线程访问相同的对象或者变量时,那么就会出现同步(Synchronization)的问题。

为什么产生同步?

让我们看一个迷宫游戏,任何线程都可以设置玩家的位置,任何一个线程都可以检查玩家是否还存在。假设,玩家处理位置是x = 0, y = 0.

Java游戏编程不完全详解-1

  • isAtExit()--判断玩家是否存在

  • setPosition()--设置玩家当前位置

以上代码在大多数情况是运行正常的,但是考虑到线程会被操作系统在任意时刻悬空,如果出现这种情况,有一个玩家从(1,0)移到(0,1)位置:

  1. 出现点对象的变量是playerX = 1和playerY = 0

  2. 线程A呼叫setPosition(0,1)

  3. 代码playerX = x被执行,现在playerX = 0

  4. 线程A被悬空让位给线程B

  5. 线程B呼叫isAtExit()方法

  6. 那么现在playerX = 0并且playerY = 0, 所以isAtExit返回true值!

在这种情况下,用户玩家被会告知Over了。为解决这个问题,我们必须保证setPosition方法和isAtExit方法不能同时被执行!

为保证线程同步,我们使用关键字synchronized来实现它,它可以保证一次只运行一个方法,下面是线程安全的代码:

public class Maze{ private int playerX; private int playerY; public synchronized boolean isAtExit(){ return (playerX == 0 && playerY == 0); } pubic synchronized void setPosition(int x, int y){ playerX = x; playerY = y; }}

  

当JVM执行同步方法时,它需要一个该对象的锁(lock)。一次只能从该对象中获取一把锁。当该方法被执行完成之后,该锁会被释放,否则会抛出异常。


所以,当一个被同步的方法获取一把锁之后,其它的被同步的方法不能被运行,除非该锁被释放掉了。我们可以把这把锁想像成一个只有一个位置的公用卫生间门的锁,该卫生间一次只有一个人使用,只有当该人离开之后,该锁才是未被锁定的。


另外除了方法同步之外,对象也可以被同步。对象同步时,我们需要把任何一个对象看成锁(与方法同步的原理一样)。此时当前实例(this)就是一个锁。方法同步实际上是使用this关键实现对象同步的缩写。比如代码


public synchronized void setPosition(int x, int y){ playerX = x; playerY = Y;};

它与使用”this”关键字书写的功能一样:

public void setPosition(int x , int y){ synchronized(this){ playerX = x; playerY = y; }}

对象同步当我们需要多个锁时是非常有用的。比如我们某些事情需要上锁,而不让”this”对象上锁,或者当我们不需要让整个方法被同步时。锁可以适用所有对象,包括数组,除了原始数据类型。如果我们需要创建自己的锁,那么只需要创建一个普通对象即可:

Object myLock = new Object();synchronized (myLock){}

什么需要同步呢?任何时候只要两个或者以上的线程需要访问相同对象或者属性时,我们称这种情况叫做同步。那什么时候不使用同步呢?回答是,当我们同步我们的代码时,不要过度同步(oversynchronize)—不要同步太多的代码。因为结果会产生多线程的不必要的延迟,从而不会达到使用线程代码之后加快代码效率。比如一般使用不同步整个方法的形式来进行同步关键代码的操作。

public void myMethod(){ synchronized(this){ //下面是被同步的关键代码 } //下面是其它线程安全的代码}

另外,我们不必同步局部变量。因为局部变量是放栈里,而每个线程拥有自己的栈空间,所以它们不会产生同步问题!比如下面的方法如果使用局部变量,那么不需要被同步:

public int square(int n){ int s = n * n; return s;}

最后,不需要担心同步代码会被多个线程对象访问。如果我们只知道一些代码只被一个线程访问,那么我们不需要进行同步,因此,我们需要做好JavaDoc的注释,说明该方法是非线程安全的如果我们不知道哪个线程访问我们的代码,那么我们可以在控制台打印出该线程的名称:

  System.out.println(Thread.currentThread().getName());


避免死锁


死锁就是两个线程互相等待的状态:

  1. 线程A获取锁1

  2. 线程B获取锁2

  3. 线程B等待锁1释放

  4. 线程A等待锁2释放


我们可以看见两个线程在等待彼此对方释放锁,所以,双方都会产生停止状态—不作为。死锁出现在多个线程试图无序的获取多个锁的情况。那么怎样避免死锁情况?最佳方式是简化同步代码的书写,但是即使这样也不可以避免死锁问题。所以,多线程编码需要仔细设计线程以什么样的顺序获取锁—必须尽可能的小心设计!


如果我们觉得游戏程序可能出现了死锁情况,那么在1.4.1是HotSpot VM,所以Sun(虽然现在Java被Oracle卖了,但是Java兼容性一直都很好,我在VS Code中使用OpenJDK 15版本也能正常跑JDK 1.4版本的代码,请大家不要怀疑Java虚拟机产品的高品质性)提供了多种死锁侦测器。我们按Ctrl  + \或者Ctrl + break(Windows中),JVM会显示线程状态信息,说明线程是等待还是发现了死锁。

//Thread Apublic void waitForMessage(){ while(hasMessage == false){ Thread.sleep(100); }}//Thread Bpublic void setMessage(String message){ hasMessage = true;}

线程A会每隔100毫秒不断的检查线程B是否发送消息。这时线程A可能出现因为等待消息而过度睡觉(oversleep)的现象。


另外,如果发生多个线程等待一个消息会怎样?解决这个问题的方案是,如果让线程A在空闲时才通知线程B发送消息会,那么我们就不强迫线程A一分钟内10次查看是否有消息到达了。这样就解决了线程A过度睡觉的情况。


使用wait()和notify()方法


Sun公司提供这样的功能wait()和notify()方法,可以让我们方便的实现这样的策略。Wait()方法被使用在synchronized语句块中,当wait方法执行时,锁会被释放,而所有等待锁的线程得到通知。而notify()方法也只能使用在synchronized语句块中。Notify()方法通过所有等待相同锁的线程,如果多个线程在等待锁,那么其中线程会被JVM随机唤醒。

//Thread Apublic synchronized void waitForMessage(){ try{ wait(); }catch(InterruptedException ex){}}//Thread Bpublic synchronized void setMessage(String message){ notify();}

在线程B呼叫notify之后,然后离开同步方法(释放该锁),线程A重新获取该锁,然后完成它的同步代码块。上面的示例只是返回而已。使用wait方法时,我们可以指定等待的最大时间值

wait(100);

表示100毫秒中不要被唤醒,这等同于呼叫了sleep方法。不好之处是:指定了wiat的线程在时间到之前不能指定结束,或者指定被唤醒。


notifyAll()方法是唤醒所有等待锁的线程,而不只唤醒一个等待的线程。因这些方法都属性Object类的,所以任何Java对象都可以被当成一把锁。


Java事件模型


不要看见有些书说Swing编程是单线程。但是实际不是这样,因为所有图形应用都至少有两个线程:主线程和AWT事件分发线程存在。主线程就是我们书写程序的主线程,它开始于我们书写主类(public类)中的main()方法。


AWT事件分发线程处理用户的输入事件:鼠标点击、键盘按下/释放,以及其它事件,比如窗体缩放等。这些事件可以访问我们的代码,它访问的方式是通过AWT事件分发线程来实现的!


注:关于Java的图形编程,我们在这里不会讨论,详细内容可以参见老九学堂创始人窖头的B站视频Java图形编程 。


无论什么时候使用线程都可以给用户带来更多的体验。也就是说,任何时候一些代码都可以停止或者持续更长的时间,因为让我们的代码在另外一个线程运行,这样我们的用户不会认游戏停止了。使用线程的情况如下:

  1. 当从本地文件系统装载许多文件时

  2. 当进行任何网络通信,比如发送高考分数到服务器

  3. 当进行海量级运算时,比如地形运算


那么什么时候不使用线程呢? 在游戏中有很多是一次性的事件,比如敌人跑开了,门打开了,子弹分飞等。这会导致一些人认为“我认为每个敌人都运行在自己的线程中”。其实不是这样,因为它浪费时间资源—一次运行太多的线程会耗尽系统的内存资源。如果这样书写代码可能产生以下问题:

  1. 一个敌人可能处理操作的中间区域,表示这种效果会一次在两个地方表示该敌人

  2. 每个线程的时间碎片可能不平衡,会导致敌人移动不协调

  3. 同步代码可能会导致不必要的延迟

处理这些问题时,我们在第二篇文章中会有更有效的方法来解决。

线程池

使用以上知识点,我们来创建一个线程池(thread pool)。一个线程池是一组线程,它们被用来执行任意任务。当然,如果我们用来模拟网络或者I/O连接时会限制它的数量,或者对于完成处理器级别的任务会限制它们的最大数目。

ThreadPool myThreadPool = new ThreadPool(8);myThreadPool.runTask(new Runnable(){ public void run(){ System.out.println(“Do something cool here.”); }});myThreadPool.join();

runTask方法会立即返回,如果池中的所有线程都忙于执行任务,那么呼叫runTask()方法时会把一个新任务放到队列中,直到一个线程可以来运行它。

线程池代码演示

代码演示环境

  • 软件环境:Windows 10

  • 开发工具:Visual Studio Code

  • JDK版本:OpenJDK 15

ThreadPool(线程池)工具类

import java.util.LinkedList;
/** 功能:一个线程池是一组有限数量的线程,它们被用来完成执行任务 翻写作者:技术大黍 备注: 线程池使用ThreadGroup API来实现.线程组表示一个线程的集合。 此外,线程组也可以包含其他线程组。线程组构成一棵树,在树中, 除了初始线程组外,每个线程组都有一个父线程组。允许线程访问 有关自己的线程组的信息,但是不允许它访问有关其线程组的父线 程组或其他任何线程组的信息。
*/public class ThreadPool extends ThreadGroup { private boolean isAlive; //表示线程是否活首 private LinkedList taskQueue; //定义一个双向队列 private int threadID; //保存线程的ID private static int threadPoolID; //表示线程池的ID
/** 在构造方法创建线程池 @参数numThreads用来指定池中的线程个数 */ public ThreadPool(int numThreads) { super("线程池-" + (threadPoolID++)); setDaemon(true);//让该线程组为精灵线程组 isAlive = true;//设置初始值为true taskQueue = new LinkedList();//初始化任务队列 for (int i=0; i<numThreads; i++) { new PooledThread().start();//启动池中的线程 } }

/** 功能:请求一个新的任务来运行,该方法立即返回。然后任务在下一下空闲 的线程中执行。该方法是线程安全的。 <p>任务开始执行时有序的到达时开始。 @参数task表示运行的任务。如果不null,那么没有任务执行。 @如果本ThreadPool关闭了,那么抛出IllegalStateException。 */ public synchronized void runTask(Runnable task) { //如果线程池的状态isAlive==false值 if (!isAlive) { throw new IllegalStateException();//那么抛出异常 } //如果任务不为null if (task != null) { //那么在任务把该任务加入任务阶段 taskQueue.add(task); //然后唤醒空闲的线程执行该任务 notify(); }
}
/** 功能:获取任务对象 */ protected synchronized Runnable getTask() throws InterruptedException{ //如果任务队列不是空的 while (taskQueue.size() == 0) { //如果线程池的状态isAlive==false值 if (!isAlive) { return null; //那么返回null值 } wait();//否则等待任务出现(添加任务) } //否则任务队列中的一个任务对象 return (Runnable)taskQueue.removeFirst(); }

/** 功能:关闭该线程池并且立即返回。让所有线程停止执行,并且所有等待任务停止执行。 一旦一个ThreadPool被关闭了,那么该线程池中的所有的线程不再运行。 */ public synchronized void close() { //如果线程池是活的 if (isAlive) { //那么置为false isAlive = false; //然后把任务队列清空 taskQueue.clear(); //最后终止线程池中所有线程的运行 interrupt(); } }

/** 功能:关闭该ThreadPool活动,然后等待所有的线程运行完成。这样所有等待的任务会被执行。 */ public void join() { // 当ThreadPool不再活动时唤醒所有等待的线程 synchronized (this) { isAlive = false; notifyAll(); } // 然后等待所有池中的线程对象执行完毕 Thread[] threads = new Thread[activeCount()]; //创建所有池中的活动线程 //把此线程组及其子组中的所有活动线程复制到指定数组中 int count = enumerate(threads); //然后按序让每个线程执行完毕 for (int i=0; i<count; i++) { try { threads[i].join(); } catch (InterruptedException ex) { } } } /** 功能:定义线程池中的线程,这些线程对象用来运行任务对象(Runnables) 翻写作者:技术大黍 备注: PooledThread类是Thread类对象,它是一个内部类 */ private class PooledThread extends Thread { public PooledThread() { //呼叫父类的构造方法Thread(ThreadGroup group, String name), //把该线程分配指定的线程组对象中去 super(ThreadPool.this,"池中线程-" + (threadID++)); } //重写run方法,执行任务对象 public void run() { //如果本线程是没有断的--isInterrupted方法测试线程是否已经中断。线程的中断状态不受该方法的影响。 //对于isInterrupted方法,如果该线程已经中断,则返回 true;否则返回 false while (!isInterrupted()) { //那么从线程池中获取一个任务对象来执行 Runnable task = null; try { task = getTask(); }catch (InterruptedException ex){ ex.printStackTrace(); }
//如果getTask()返回null或者被中断了,那么使用return语句来关闭该线程 // close this thread by returning. if (task == null) { return; }
//否则执行任务对象,并且消除所有异常信息 try { task.run(); }catch (Throwable t) { //呼叫ThreadGroup类的静态方法处理异常 uncaughtException(this, t); } } } }}

ThreadPoolTest测试类

import static java.lang.System.*;/** 功能:书写一个测试类线程池的类 作者:技术大黍 */public class ThreadPoolTest {  public static void main(String[] args) { if (args.length != 2) { out.println("测试ThreadPool(线程池)任务."); out.println( "使用方法: java ThreadPoolTest 任务数 线程数"); out.println( " 任务数 - integer: 表示需要执行的任务数量."); out.println( " 线程数 - integer: 表示在线程池中的线程的数量 "); return; } //读取命令行参数任务数值和线程数值 int numTasks = Integer.parseInt(args[0]); int numThreads = Integer.parseInt(args[1]);  //创建线程池对象 ThreadPool threadPool = new ThreadPool(numThreads);  //执行示例任务 for (int i = 0; i < numTasks; i++) { threadPool.runTask(createTask(i)); }  //关闭线程池以等待所有线程完毕 threadPool.join(); }   /** 功能:创建简单Runnable对象用来每隔500毫秒打印ID */ private static Runnable createTask(final int taskID) { return new Runnable() { public void run() { out.println("任务 " + taskID + ": 开始");  //模拟长时间执行任务 try { Thread.sleep(500); }catch (InterruptedException ex) {  ex.printStackTrace(); } out.println("任务 " + taskID + ": 结束"); } }; }}

运行效果


Java游戏编程不完全详解-1


总结


可能有的小伙伴会认为,这种JDK 1.4版本的Java代码写法看上没有现在的Java 8及以后版本的高大上啊,应该是过时的代码。


如果各位看官一定要这样认为,那我就只有呵呵,不解释了。不过,各位看官们是可以使用Java 8中的多线程框架API来尝试改写,我相信这是一个很好的学习方法。为了帮助大家可以使用Java 8的多线程API,我们补充讲解两个多线程并发框架。


补充Fork/Join框架


在《Mastering Concurrency Programming with Java 8》一书中介绍一个Fork/Join的并发框架

Java游戏编程不完全详解-1我来参考翻译一下,请大家不要当成标准,如果不足之处,请指正和补充:

Fork/Join框架定义了一种特别的执行器,它是一种切分和竞争技巧。该框架包含了针对执行器并发任务的优化机制 。Fork/Join是最小的细颗粒度并发单位,它拥有非常小的负载运行效率来运行新的任务,而这些任务是需要放到队列中,并且通过队列化操作来执行的。该框架包含的主要的类和接口如下:


  • ForkJoinPool--该是一个运行任务的线程池

  • ForkJoinTask--在ForkJoinPool类运行的任务

  • ForkJoinWorkerThread--在ForkJoinPool类中执行任务的线程


ForJoinPool使用示例代码


import java.util.*;import java.util.concurrent.*;/** * 功能:书写一个测试类,用来演示ForkJoinPool类的用法。 * 作者:技术大黍 */
public class ForkJoinPoolTest { public static void main(String[] args) { //创建线程池对象 ForkJoinPool pool = new ForkJoinPool(); IntSum task = new IntSum(3); long sum = pool.invoke(task); System.out.println("Sum is " + sum); }}/** * 功能:书写需要被并发计算整数的类 */class IntSum extends RecursiveTask<Long> { private int count; public IntSum(int count) { this.count = count; }
@Override protected Long compute() { long result = 0;
if (this.count <= 0) { return 0L; }else if (this.count == 1) { return (long) this.getRandomInteger(); } List<RecursiveTask<Long>> forks = new ArrayList<>(); for (int i = 0; i < this.count; i++) { IntSum subTask = new IntSum(1); subTask.fork(); // 启动子任务 forks.add(subTask); } // 所有子任务完成并且组成一个结果 for (RecursiveTask<Long> subTask : forks) { result = result + subTask.join(); } return result; }
public int getRandomInteger() { return 2; }}

这种经过Java封装的多线程代码,我们看上去是不是有眼一亮,很清爽的感觉?我相信大家肯定是这种感觉,因为语法给我们感觉就是一个单线程的代码三,很好理解,也不复杂嘛Java游戏编程不完全详解-1。 在VS Code中的运行效果如下:


Java游戏编程不完全详解-1


补充Executors框架


在《Mastering Concurrency Programming with Java 8》一书中介绍一个Executor的并发框架


Java游戏编程不完全详解-1


我来参考翻译一下,请大家不要当成标准,如果不足之处,请指正和补充:


执行器框架是一种分隔线程的机制,这种机制包含了针对线程的创建和管理,以实现并发任务的效果。我们第三方使用者不用担心线程的创建和管理工作,只需要创建一个任务并且把这些任务对象放到执行器中去就完了。该框架包含的主要类如下:


  • Exectutor和ExecutorService接口定义了所有执行器的接口行为

  • ThreadPoolExecutor类允许我们第三方使用者从线程池中获取一个执行,并且可以指定最大任务并发数

  • ScheduledThreadPoolExecutor它是一个特殊的执行器,根据我们实际场景要求来调试执行的任务

  • Executors是创建执行器的工具类

  • Callable接口是实现Runnable接口的替代方案--执行一个任务后并且返回一个值(也就是C/C++中的回调函数,这个接口就是Java 5版本引入的新的多线程API,也就是Java从纯面向对象编程思想向函数思想的转变的见证)

  • Future接口是与Callable配套实现回调函数的接口。


Excutors使用示例代码


import java.util.concurrent.*;
/** * 功能:书写一个Excutors类实现线程池的示例 * 作者:技术大黍 */public class ExcutorsTest { public static void main(String[] args) { final int THREAD_COUNT = 3; final int LOOP_COUNT = 3; final int TASK_COUNT = 5;
// 从线程池中获取一个执行器对象 ExecutorService exec = Executors.newFixedThreadPool(THREAD_COUNT);
// 创建5个任务对象,然后把它们放到执行器中去 for (int i = 1; i <= TASK_COUNT; i++) { RunnableTask task = new RunnableTask(i, LOOP_COUNT); exec.submit(task); } exec.shutdown(); }}
/** * 功能:书写一个被线程池执行的任务对象 */class RunnableTask implements Runnable { private int taskId; private int loopCounter;
public RunnableTask(int taskId, int loopCounter) { this.taskId = taskId; this.loopCounter = loopCounter; }
public void run() { for (int i = 1; i <= loopCounter; i++) { try { System.out.println("Task #" + this.taskId + " - Iteration #" + i); Thread.sleep(1000); } catch (Exception e) { System.out.println("Task #" + this.taskId + " has been interrupted."); break; } } }}

唉~ 我们看完段代码后,是不是一种觉得很没有技术含量的感觉呢?

我们看一下在VS Code中的运行效果



好简单啊~ 但是,为毛面试要问底层实现呢?这是一个值得我们思考的问题。


如果大家有兴趣,那么希望大家能够使用这些补充的知识点来重写前面的线程池代码。并请大家继续参看下一篇“Java游戏编程不完全详解-2”。


记得给大黍❤️关注+点赞+收藏+评论+转发❤️