发布时间:2024-08-16
浏览次数:0
如果你家里有邪恶的东西,那一定有鬼
大家好,我是为哥。
在《深入理解Java虚拟机》这本书里有一段代码:
public class VolatileTest {
public static volatile int race = 0;
public static void increase() {
race++;
}
private static final int THREADS_COUNT=20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for(int i = 0; i < THREADS_COUNT; i++){
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
}).start();
}
//等待所有累加线程都结束
while(Thread.activeCount()>1)
Thread.yield();
System.out.println(race);
}
}
当你看到这段代码时你的第一反应是什么?
重点是关键词吗?
我差点要脱口而出:只保证可见性,不保证原子性。而且代码里的 race++ 也不是原子操作,等等等等……
我的情况是这样的:
当他把代码发给我后,我将其粘贴到idea中,运行main方法,神奇的事情发生了。
这段代码实际上并没有执行输出语句,因此并没有任何错误。
这看上去像是一个死循环。
如果你不相信,你也可以把它写进你的想法中去,去执行。
ETC......
死循环?
代码中不是有一个无限循环吗?
//等待所有累加线程都结束
while(Thread.activeCount()>1)
Thread.yield();
这段代码背后隐藏着什么目的?它看上去无害。
但我的程序员直觉告诉我这里存在问题。
活跃线程数永远大于1,所以while循环永远处于无限循环状态。
算了,还是不多想了,先debug看看吧。
调试了两次之后,发现这个东西还是挺有意思的。
因为在Debug模式下,程序正常结束。
这是怎么回事?
让我们开始进行一些分析。
我为何停不下来?
我该如何分析这个问题?
我再次运行程序,控制台仍然没有任何输出。
我只是盯着控制台并想知道原因是什么。
这样做似乎不是一个解决办法。
不管怎样,我现在确信这个while循环有问题,所以我想排除其他干扰因素。
我将程序简化如下:
public class VolatileTest {
public static volatile int race = 0;
public static void main(String[] args) {
while(Thread.activeCount()>1)
Thread.yield();
System.out.println("race = " + race);
}
}
运行之后,输出的语句还是没有执行完,间接证实了我的想法:while循环有问题。
while循环的条件是.()>1。
继续沿着这个方向思考,我们可以看到有多少个活跃的线程。
因此该程序可以简化如下:
直接运行就会看到输出是2。
在调试模式下运行时,返回1。
对比一下这些运行结果,我基本上心里有一个想法了。
我们先来看看这个方法做了什么:
注意下划线区域:
返回值为一。
它是什么?
瞧,你又跟我学了一次高级词汇。真好。
返回的是一个估计值。
为什么?
因为我们调用这个方法,得到值之后,线程数还在动态的变化。
也就是说,返回值仅仅代表你调用它时有多少个活跃的线程。也许当你完成调用时,其中一个线程就立即死了。
因此,该值是一个估计值。
这时,我突然想到了量子力学中的不确定性原理。
你不可能同时知道一个粒子的位置和速度,就像在多线程高并发的情况下你不可能同时知道调用一个方法得到的值和你想用的时候这个值的实际值。
你看,我刚刚学完英语,现在正在学习量子力学。
好的,回到程序。
虽然注释上说返回值是,但是我们的程序中不存在这个问题。
看到该方法的实现之后:
public static int activeCount() {
return currentThread().getThreadGroup().activeCount();
}
然后我就想,既然直接运行程序返回的数字是2,那我看看有哪些线程?
其实我最开始是想调试一下的,但是在调试的情况下intellij idea找不到图标,返回的数字是1,我意识到这个问题一定和idea有关,只好用日志调试来查找原因。
因此,我将程序改成这样:
直接运行可以看到确实有两个线程。
一个是我们熟悉的主线程。
一个是Ctrl-Break线程,我不认识它。
但是当我在调试模式下运行它时,发生了一些有趣的事情:
Ctrl-Break 线程消失了!?
于是我问他:
是的,问题解决了,但是为什么呢?
为什么Run不能运行,但是Debug可以运行呢?
当前主题是什么?
我们先来理一下现在的线程有哪些。
您可以使用以下代码来获取所有当前线程:
public static Thread[] findAllThread(){
ThreadGroup currentGroup =Thread.currentThread().getThreadGroup();
while (currentGroup.getParent()!=null){
// 返回此线程组的父线程组
currentGroup=currentGroup.getParent();
}
//此线程组中活动线程的估计数
int noThreads = currentGroup.activeCount();
Thread[] lstThreads = new Thread[noThreads];
//把对此线程组中的所有活动子组的引用复制到指定数组中。
currentGroup.enumerate(lstThreads);
for (Thread thread : lstThreads) {
System.out.println("线程数量:"+noThreads+" " +
"线程id:" + thread.getId() +
" 线程名称:" + thread.getName() +
" 线程状态:" + thread.getState());
}
return lstThreads;
}
运行之后可以看到有6个线程:
也就是说在idea中,一个main方法运行之后,哪怕它什么都不做,也会有6个线程在运行。
这6个线程起什么作用?
让我们一一讨论一下。
主题:
JVM在创建主线程之后,会创建线程,其优先级最高,为10,主要用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收问题。
主题:
这个线程也是在主线程之后创建的,优先级为10,主要用于垃圾回收前调用对象的()方法。
关于线程的几点:
1)只有一轮垃圾收集开始时, () 方法才会被调用;因此,并不是所有对象的 () 方法都会被执行;
2)该线程也是一个线程,因此如果虚拟机中没有其他非线程的话,无论该线程是否完成了()方法的执行,JVM都会退出;
3)垃圾回收的时候,JVM会把失去引用的对象打包成一个对象(实现)放进去,交由线程来处理;最后将该对象的引用设置为null,由垃圾回收器进行回收;
4)为什么JVM要用单独的线程来执行()方法? 如果让JVM的垃圾收集线程自己来做的话,很有可能因为在()方法中误操作而导致GC线程停止或者不可控,这对于GC线程来说就是一场灾难。
主题:
线程负责接收外部的命令,执行命令并将结果返回给发送者,通常我们会用一些命令来要求JVM给我们一些反馈信息。
如:java -、jmap等。如果在jvm启动时没有初始化线程,那么在用户第一次执行jvm命令时就会启动该线程。
主题:
上面提到了第一个线程负责接收外部的JVM命令,当命令接收成功后会交给该线程分发给不同的模块去处理命令并返回处理结果,线程在第一次接收外部的JVM命令时也会进行初始化工作。
主线程:
好了,我们就不说这个了。大家都知道。
Ctrl-Break 线程:
我现在先让你保持悬念intellij idea找不到图标,并在下一节中讨论这个话题。
上述线程的功能来自这个网页。还有许多其他线程,你可以查看它们:
我要把好事坚持到底,所以我只会给你一张长截图来捕捉它们。
先把图片保存下来,稍后再看:
现在跟随我来探索Ctrl-Break线程的秘密。
继续挖掘
问题解决了,但是背后的问题还没有解决。
什么是 Ctrl-Break 线程?它是如何产生的?
我们先来看一下线程堆栈。
在idea中,这里的“相机”图标具有相同的功能。
我将程序恢复到原始状态,然后单击“相机”,如下所示:
从线程堆栈中可以看到Ctrl-Break线程来自这个地方:
com..rt...$1.运行(.java:64)
而这个地方,顾名思义,就是idea的源代码了?
它不再属于我们的项目了,我们该怎么办?
想来想去,想到了一个可能,于是决定用jps命令来验证一下:
当我看到执行结果时我笑了,一切都说得通了。
果然用了——啊。
那么它是什么?
当你在命令行上执行 java 命令时,会输出一长串的内容,其中包括:
我不明白代理人在说什么语言。
请参考 java.lang。
那么它是用来做什么的呢?
一个简单的解释是:
使用字节码增强技术可以更加方便,可以认为是JVM级别的横切面,可以在不侵入程序源代码的情况下对程序源代码进行增强或者修改,总之有点AOP的味道。
-命令后面需要跟jar包。
-:[=]
该机制要求这个jar包里必须有一个.MF文件,且.MF文件中必须包含-Class。
那么,让我们回到我们的程序并看看后面的包是什么。
我可以在哪里观看?
就在这里:
点击它,命令很长。但我们关心的就在开头:
-:D:\文件\\ IDEA 2019.3.4\lib\.jar=61960
可以看到在文件目录下找到了如下的jar包,就是在这里:
我们解压这个jar包,并打开它的.MF文件:
而这个类正是我们要找的:
此刻,我们距离真相仅剩一步之遥。
进入对应的包,发现三个类:
主要关注.class文件:
在这个文件中,有一个方法:
我说了啥?
来,大声重复给我听:源代码下没有秘密。
这就是 Ctrl-Break 线程的由来。
仔细看看这里的代码,这个线程在做什么?
=新的(“127.0.0.1”,);
天哪,看看这个可爱的小东西。编程太熟悉了,让我想起了大学的实验课。
它连接到 127.0.0.1 上的端口,然后在一段时间(true)无限循环中等待接收命令。
那么这是哪个端口?
这里是 62325:
需要注意的是,这个端口并不是固定的,每次启动都会改变。
玩一玩
既然是编程,那我就玩玩吧。
首先制作一个程序:
public class SocketTest{
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(12345);
System.out.println("等待客户端连接.");
Socket socket = serverSocket.accept();
System.out.println("有客户端连接上了 "+ socket.getInetAddress() + ":" + socket.getPort() +"");
OutputStream outputStream = socket.getOutputStream();
Scanner scanner = new Scanner(System.in);
while (true)
{
System.out.println("请输入指令: ");
String s = scanner.nextLine();
String message = s + "\n";
outputStream.write(message.getBytes("US-ASCII"));
}
}
}
我们将服务器端口指定为12345。
客户端的端口也需要指定为12345,那么如何指定呢?
不要想得太复杂,保持简单就好。
粘贴这行日志:
需要说明的是,为了演示效果,我在程序中添加了for循环。
然后我们这里将端口改为12345:
将文件保存为start.bat并将其放在任何地方。
一切就绪。
我们首先运行服务器:
然后,执行bat文件:
cmd窗口里有我们的log输出,说明程序运行正常。
在服务器端则显示客户端连接成功。
让我们输入命令。
我应该输入什么命令?
我们看看客户端支持哪些命令:
可以看到,STOP命令是受支持的。
程序接收到该命令后,将退出。
来吧,让我们做一些动画:
完毕。
好了,这篇文章的技术部分就讲到这里,恭喜你了解了idea中的Ctrl-Break线程,这是没用的知识。
如果要挖得更深,就朝-方向挖。
有很多应用程序,例如著名的Java诊断工具就是基于它的。
相当有趣。
—EOF—
如有侵权请联系删除!
Copyright © 2023 江苏优软数字科技有限公司 All Rights Reserved.正版sublime text、Codejock、IntelliJ IDEA、sketch、Mestrenova、DNAstar服务提供商
13262879759
微信二维码