Part1 前言
大家好,我是ABC_123。之前编写工具的图形界面都是用swing框架来实现,但是swing框架已经10几年没有更新了,很多控件使用起来特别麻烦,然后界面美工需要花费很大精力。为了跟上知识更新的节奏,ABC_123最近花时间学习了javafx(有swing的基础,学习javafx上手是非常快的),于是花时间把之前的扫描工具的图形界面换成了javafx,但是在多线程操控图形界面控件的时候,遇到了一系列线程死锁问题,为了解决这些问题,ABC_123又踩了一大堆坑,今天当做笔记,自我复盘的同时,也把经验分享给大家。
Part2 技术研究过程
扫描器设计思路
我想实现如下功能:burpsuite抓到一个数据包之后,点击右键弹出菜单,将指定的扫描任务发送到服务端的“扫描任务队列”去进行扫描,与服务端通信是通过socket实现的。
“扫描任务队列”会监听一个端口,收到burpsuite的任务请求之后,会新建一个Tab标签,然后每个任务分配10个线程扫描,也就是说,每一个Tab标签对应着一个扫描任务,每个扫描任务都是10个线程在运行。在编写这个扫描工具过程中,踩了一大堆坑,接下来把解决方法分享给大家。
坑1:多线程中添加一个Tab标签直接报错
刚开始用多线程操作javafx控件就遇到了一个报错,向图形界面添加一个图形控件时,报错提示“Not on FX application thread; currentThread = Thread-3”,大致意思是“当前线程不是JavaFX应用程序线程”。
经过一系列搜索发现,操控javafx的图形控件需要用以下Java语句包裹起来就可以了Platform.runLater(() -> { });。
坑2:Platform.runLater与ReentrantLock可重入锁的选择问题
进过前面探讨我们知道,Platform.runLater保证javafx线程安全,ReentrantLock锁可以保证全局变量的线程安全问题。这就引出一个问题,对于如下代码,当多线程操控qq.readResCount = qq.readResCount + 1;这个全局变量的值时,它本身已经被Platform.runLater(() -> {});包裹了,用不用加上再ReentrantLock锁保证线程安全呢?
在网上各种百度谷歌,然后询问身边的人,对这个问题说法不一,所以我干脆就自己编写一个测试代码,实战测试一下吧。
1 全局变量不加锁的错误写法
首先回顾一下多线程资源竞争问题,如下代码运行之后出现错,因为多线程操控全局变量没有任何限制,很明显会出现竞争问题。正常输出是7、8、9、10随机出现,但是却出现了多个10及多个11的情况,输出结果明显不正确。接下来分情况测试一下,探究一下Platform.runLater与ReentrantLock锁应该怎么配合使用。
2 Platform.runLater不用,ReentrantLock锁使用
首先看这种情况,运行后马上各种报错,说明ReentrantLock锁无法保证javafx控件的线程安全问题。
3 Platform.runLater使用,ReentrantLock也使用
接下来看这种情况,运行后非常稳定,没有问题,但是对于Quanjv.count全局变量的改变,ReentrantLock锁是否可以去掉呢?
4 把ReentrantLock锁去掉
接下来看这种情况,把ReentrantLock锁去掉,由Platform.runLater保护Quanjv.count,发现程序运行之后,没有问题,说明Platform.runLater在保证javafx控件安全时,也能保证全局变量的线程安全。
通过以上的测试,最终我们得出一个结论:
1. Platform.runLater(() -> {});不但可以保证Javafx控件线程安全,同时也可以保证全局变量数据的线程安全。
2. ReentrantLock锁可以保证全局变量数据的线程安全,但是对于保证javafx控件线程安全毫无用处。
坑3:javafx控件取值和修改值是否需要加锁
在网上搜索了很多说法,答案不一,那我们还是编写测试代码,来测试一下吧。
1 javafx控件取值过程测试
为了保证测试效果,我们设置100个线程同时操作textThread方法,高并发可以提升线程安全问题报错的机率。经过测试我们发现,对于TextArea的多线程取值过程,不用加Platform.runLater(() -> {});,也可以保证线程安全。
2 javafx控件修改值过程测试
接下来再添加一行修改javafx控件文本框的代码:Quanjv.textarea.setText("test");,发现在100个线程操作下程序立马报错。
接下来对修改javafx值的代码用Platform.runLater(() -> {});包裹起来,程序运行之后发现,100个线程下没有任何错误。
最终得出结论,javafx的控件的取值过程基本上不涉及线程安全问题,但是对于javafx组件的任何修改,必须考虑线程安全问题。
坑4:Tabs标签移除问题
当发送一个扫描任务队列时,TabPane会新建一个Tab标签,每个标签10个线程运行,双击Tab标签,就会停止该任务的多线程扫描,Tab标签的标题会提示“停止..”字样,直到所有活动线程安全结束,该标签关闭。
代码是按照如下格式编写的,用Platform.runLater(() -> {});代码包裹起来,按理上不存在线程安全问题。
但是实测结果,经常在如下代码中,出现报错问题,导致程序崩溃,所有扫描任务停止。
这是一个隐藏非常深的线程安全bug,在一天中会不定时的出现几次,而且没办法复现,让我大伤脑筋。后来我终于想明白了,一个TabPane是由多个标签组成的,当你双击关闭其中一两个标签时,tabPane的所有索引id都变了,而另一个线程对于Tab标签的for循环操作还在进行当中,而且还是按照原始的索引去遍历,而原始的索引都变了,造成了程序的崩溃。
坑5:jdk8与jdk11等高版本不兼容
举个例子,对于以下这个图形界面,是使用scenebuilder20.x版本拖拽出来的,看着没有问题。
但是如果用sceneBuidler 8.x版本打开,整个界面的很多控件的位置都乱了,重叠在一起。
最终得出结论:javafx的图形界面在jdk8及其它高版本jdk是存在兼容性问题的,Scenebuilder8.x适用于jdk8版本的图形界面拖拽,Scenebuilder20.x适用于jdk11到jdk20的版本的图形界面拖拽。
坑6:fmxl行数过多会很卡
用Scenebuilder拖拽的方法画图形界面,感觉特别方便,但是也有问题。比如说我写的如下工具,fxml文件已经快1500行了,此时再用scenebuilder拖拽会特别卡。
最终没有办法,我将其中一个TabPane界面的Tab标签删掉,用纯java代码编写,有时候用纯java代码写图形界面比拖拽是要方便的。以下这个界面,按钮控件特别多,每个按钮的功能类似,于是我用一个Map集合放置每一个按钮标题和按钮事件中用到的关键值,然后用一个for循环,遍历Map集合添加Button按钮组件,很快搞定这个界面,比Scenebuilder拖拽图形界面还方便。
我们也可以发现,通过java纯代码编写的图形界面,比Scenebuilder拖拽的看起来要规整,因为很多时候拖拽会在控件对齐方面会有误差,这就是java代码编写图形界面的好处。
坑7:javafx在jdk11至jdk17的编译问题
按照正常的编写javafx程序的流程,idea 2022版本编译出来的jar包,有时候会提示找不到主类,有时候会提示缺少JavaFX运行组件。对于jdk8下的javafx的编译,很简单,直接编译成一个jar包就可以在jdk8上双击运行,因为jdk是自带javafx库的,但是对于更高版本的jdk,比如说jdk11或者jdk17,默认是不带javafx库的,所以就引发出各种各样的问题。
网上有很多解决这个问题的方法,但是说法不一,于是我经过各种测试,得出如下步骤,可以保证编译的jar包能够正常运行。
首先使用idea 2022新建项目,JDK选择大于等于jdk8的版本即可,小于jdk8不支持javafx。
可以看到idea 2022版本,已经自动在pom.xml文件中添加了javafx库了。所以我们无需添加额外的javafx的jar包,有的解决方案说是要从javafx官网下载jar包导入,实际上是没必要的。
接下来是最重要的一个步骤,我们需要新建一个主类,按照如下格式编写:
接下来需要设置如何去编译jar包文件,主类需要选择我们新建的JavaFXBootstrap类,记住一定要删掉main\resources。
如下图所示,这是正确的idea配置。按照上述的操作编译出来的jar包,可以完美运行而不报错。
Part3 总结
1. 遇到线程安全问题,最好的方法就是写个demo程序在高并发下反复测试。
2. 其余的总结及结论都在文章里每一部分给出了,这里不再重复。
公众号专注于网络安全技术分享,包括APT事件分析、红队攻防、蓝队分析、渗透测试、代码审计等,每周一篇,99%原创,敬请关注。
Contact me: 0day123abc#gmail.com(replace # with @)