前言

在游戏开发领域,近些年大量的项目开始转向go语言开发,反而java作为游戏服务器开发的岗位越来越少。很大一部分原因就是go语言的协程,简化了设计高并发架构的难度。在jdk21,java也正式发布了虚拟线程,而基于虚拟线程的游戏服务器又该如何设计,如何才能达到更高的并发。这里博主做了些许思考,如果有不同的意见,欢迎讨论。


传统的并发设计

在虚拟线程出现之前,多线程必定是处理并发的唯一途径。多线程的问题在于,线程数量如何设定,线程数量少了,不能达到并发量,线程数量多了,造成内存浪费,以及线程切换的cpu浪费。所以我们的性能跟我们的线程数是一个抛物线关系。如图:(自己手画的,请见谅)

JAVA虚拟线程解决游戏服务器高并发的探索_游戏服务器

并发量峰值的线程数是不固定的,跟我们的io阻塞时间有关,具体的计算方法,网上有文章介绍。但不管怎么计算都是一个大概值,因为我们系统的io阻塞时间本身就是不固定的,跟我们执行的业务有关,跟我们网络有关,跟我们数据库性能有关。

有的同学就会发现,既然我们的性能瓶颈是io阻塞,那我们有没有可能解决io阻塞呢。在游戏中,的确有很多这种设计,他们怎么去解决io阻塞呢,第一个玩家登录时候,把玩家的所有数据加载到内存,后续所有的业务都操作内存,第二个就是业务只有一个逻辑服,不存在服务间的业务调用,或者很少的业务调用。 这种设计的确是快,但是缺点也很明显,数据都是写内存,持久化是其他线程定时处理,那必然不是实时的持久化,服务器宕机就会丢失数据,这种设计也无法微服务化,只能做单节点,因为数据在内存,没办法共享。

还有一个解决io阻塞的分支,就是java的响应式编程,这其中的优缺点,网上也有很多讨论。主要还是让代码非常的混乱,难以理解和调试,这个在游戏中的使用并不多。而且响应式是否真的能提高性能,还是说只是在某些特定条件下提高性能,还有待确定。

java中有个高并发框架,叫做akka,个人认为这个设计才是多线程处理高并发最正统的方案。具体实现可以查看官网。我们今天的重点是虚拟线程。

虚拟线程处理高并发

虚拟线程的使用,以下是几种的使用方法

// 使用静态构建器方法
Thread virtualThread = Thread.startVirtualThread(() -> {System.out.println("run");} )
  
// --------------------- 分割线 ----------------------- //
Thread.ofVirtual().name("didispace-virtual-thread").start(() -> {System.out.println("run");} );

// --------------------- 分割线 使用 ExecutorService ----------------------- //
try (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()) {    
  for (int i = 0; i < 100; i++) {        
    executorService.submit(() -> {System.out.println("run");});    
  }
}

// --------------------- 分割线 使用虚拟线程工厂 ----------------------- //
ThreadFactory virtualThreadFactory = Thread.ofVirtual()
  .name("didispace", 0)        
  .factory();
Thread factoryThread = virtualThreadFactory.newThread(() -> {System.out.println("run");});
factoryThread.start();

虚拟线程的使用非常简单,但是我们怎么保证玩家消息的有序性,如果不能让同个玩家消息顺序执行,必然存在并发问题。解决并发问题我们可以用两种方案,一种是锁,一种是队列。

在虚拟线程中,锁是万万不能用synchronized的,因为synchronized会让虚拟线程无法卸载,阻塞系统线程。锁的话使用ReentrantLock,并且使用公平锁,构造函数传true。因为公平锁是排队的,为每个玩家申请一个锁,拿到锁的虚拟线程运行。

使用队列,需要为每个玩家申请一个队列,消息队列中的消息顺序执行,执行完一个之后,再poll下一个执行,保证顺序。ReentrantLock的底层也是通过队列,所以队列是解决并发的最优解。

队列的回收,为每个玩家生成一个队列,如果玩家下线了,或者长时间没有消息,就需要回收队列,不收回,就会一直膨胀,直到内存溢出。这个可以进行检测,如果队列为空,时间达到十分钟,或者半个小时,则回收队列。

JAVA虚拟线程解决游戏服务器高并发的探索_游戏服务器_02

因为有了虚拟线程,所以不用担心传统多线程中线程数量的问题,为每位玩家申请一个队列,申请一个虚拟线程来执行。这样非常方便,而且不用担心玩家之间的互相阻塞。游戏中还会存在多个玩家操作相同数据的问题,第一种方案我们可以用锁,第二中方案,我们可以把这个共同数据抽象成一个新的玩家id,然后我们给这个玩家id发消息,自然会为该玩家生成新的队列,来保证顺序。

为了让这个架构更具有通用性,不对业务有任何的影响,所以进行了封装,整个虚拟线程执行逻辑只暴露一个接口


public void execute(String uid,Runnable runnable);

我们的业务逻辑在Runnable里面,虚拟线程不关心具体是什么,只需要按照传入的uid进行队列排序执行即可。总的说来,使用虚拟线程,反而让架构更简单,并且大幅提升性能。希望虚拟线程能让java在游戏开发中扳回一局。


参考资料

1、 虚拟线程官方文档

2、 虚拟线程原理及性能分析

3、 深入理解akka