使用多线程时大家是否有下面这些疑问:
Thread.sleep(0);
它的作用是什么?我们都知道CPU处理速度很快,多线程情况下CPU会按照操作系统的调度算法有序快速有序的执行任务,使得我们即使开个十几个程序,看上去所有程序仍像是在同时运行一样。
上文提到CPU执行线程时会按照任务优先级进行处理,一般而言,对于硬件产生的信号优先级都是最高的,当收到中断信号时,CPU理应中断手头的任务去处理硬件中断程序。例如:用户键盘打字输入、收取网络数据包。以用户键盘打字输入为例,从键盘输入到CPU处理的流程为:
同理,获取网络数据包的执行流程为:
了解网络数据包获取流程整个流程后,不知道读者是否发现,网卡读取数据期间CPU似乎无需参与工作的,那么操作系统是如何处理这期间的任务调度呢?
操作系统为了支持多任务,将任务分为了运行、等待、就绪等几种状态,对于运行状态的任务,操作系统会将其放到工作队列中。CPU按照操作系统的调度算法按需执行工作队列中的任务。
需要注意的是,这些任务能够被CPU时间片完整执行的前提是任务不会发生阻塞。一旦任务或是读取本地文件或者发起网络IO等原因发起阻塞,这些线程任务就会被放到等待队列中,就如图上面所有的收取网络数据包,在网卡读取数据并写入到内存这期间,该任务就是在等待队列中完成的。
只有这些IO任务接受到了完整的数据并通过中断程序发送信号给CPU,操作系统才会将其放到工作队列中,让CPU读取数据。
这也就是IO阻塞避免CPU资源消耗的原因,以及为什么IO任务使用多线程高效的原因所在。
对于上述问题,我们不妨看一段这样的代码,功能很简单,服务端开启9009端口获取客户端输入的信息。
服务端代码如下,逻辑也很清晰,执行步骤为:
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = null;
try {
// 创建服务器 Socket 并绑定 9009 端口
serverSocket = new ServerSocket(9009);
} catch (IOException e) {
System.err.println("Could not listen on port: 9009.");
System.exit(1);
}
Socket clientSocket = null;
System.out.println("Waiting for connection...");
try {
// 等待客户端连接
clientSocket = serverSocket.accept();
System.out.println("Connection successful!");
} catch (IOException e) {
System.err.println("Accept failed.");
System.exit(1);
}
//输出流
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
//输入流
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) { // 不断读取客户端发送的消息
System.out.println("Client: " + inputLine);
out.println("Server: Welcome to the server!"); // 向客户端发送欢迎消息
}
out.close();
in.close();
clientSocket.close();
serverSocket.close();
}
}
客户端代码示例如下,执行步骤为:
public class Client {
public static void main(String[] args) throws IOException {
Socket socket = null;
PrintWriter out = null;
BufferedReader in = null;
try {
socket = new Socket("localhost", 8080); // 连接到服务器
out = new PrintWriter(socket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
} catch (UnknownHostException e) {
System.err.println("Unknown host: localhost.");
System.exit(1);
} catch (IOException e) {
System.err.println("Couldn't get I/O for the connection to: localhost.");
System.exit(1);
}
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
String userInput;
while ((userInput = stdIn.readLine()) != null) { // 不断从控制台读取用户输入
out.println(userInput); // 向服务器发送消息
System.out.println("Server: " + in.readLine()); // 从服务器读取消息并打印到控制台
}
out.close();
in.close();
stdIn.close();
socket.close();
}
}
启动服务端,我们会看到这样一段输出:
Waiting for connection...
并通过客户端发送字符串hello world,服务端的输出结果如下:
Waiting for connection...
Connection successful!
Client: hello world
了解整个流程之后,我们再对细节进行分析。对于服务端的每一个步骤,CPU对应做法如下:
new ServerSocket(9009)
新建由文件系统管理的Socket
对象,并绑定9009
端口。serverSocket.accept();
阻塞监听等待客户端连接,此时CPU就会将其放到等待队列中,去处理其他线程任务。in.readLine()
阻塞获取用户发送数据,CPU再次将其放到等待队列中,处理其他非阻塞的线程任务。上文我们提到过CPU会按照某种调度算法执行进程任务,这里的算法大致分为两种:
Windows用的调度算法就是抢占算法,它会在调度前计算每一个线程的优先级,然后按照优先级执行任务,执行任务直到执行到线程主动挂起释放执行权或者CPU察觉到该线程霸占CPU执行时间过长将其强行挂起。
此后会再次重新计算一次优先级,在这期间,那些等待很久的线程优先级就会被大大提高,然后CPU再次找出优先级最高的线程任务执行。
之所以我们称这种算法为抢占式,是因为每次进行重新分配时不一定是公平的。假设线程1第一次执行到期后,CPU重新计算优先级,结果发现还是线程1优先级最高,那么线程1依然会再次获得CPU执行权,这就导致其他线程一直没有执行的机会,极可能出现线程饥饿的情况。
Unix操作系统用的就是非抢占式调度算法,即时间分片算法,它会将时间平均切片,每一个进程都会得到一个平均的执行时间,只有任务执行完分片算法分配的时间或者在执行期间发生阻塞,CPU才会切换到下一个线程执行。因为时间分片是平均的,所以分片算法可以保证尽可能的公平。
上文提到抢占式算法可能导致线程饥饿的问题,所以我们是否有什么办法让长时间霸占CPU的线程主动让CPU重新计算一次优先级呢?
答案就是Thread.sleep()
方法,通过该方法让当前线程休眠,进入等待队列,此时CPU就会重新计算任务优先级。这样一来那些因为长时间等待使得优先级被拔高的线程就会被CPU优先处理了。
对应代码如下可以看到在RocketMQ这个大循环中,处理一些刷盘的操作,该因为是大循环,且涉及数据来回传输等操作,所以循环期间势必会创建大量的垃圾对象。
所以代码中有个if判断调用了Thread.sleep(0)
,作用如上所说,假设运行Java程序的操作系统采用抢占式调度算法,可能会出现以下的一个流程:
所以设计者们考虑到这一点,这在循环内部每一个小节点时调用Thread.sleep()
,确保每执行一小段时间执行让操作系统进行一次CPU竞争,让GC线程尽可能多执行,做到垃圾回收的削峰填谷,避免后续出现一次长时间的GC时间导致STW进而阻塞业务线程的运行。
for (int i = 0, j = 0; i < this.fileSize; i += MappedFile.OS_PAGE_SIZE, j++) {
byteBuffer.put(i, (byte) 0);
// force flush when flush disk type is sync
if (type == FlushDiskType.SYNC_FLUSH) {
if ((i / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE) >= pages) {
flush = i;
mappedByteBuffer.force();
}
}
// prevent gc
if (j % 1000 == 0) {
log.info("j={}, costTime={}", j, System.currentTimeMillis() - time);
time = System.currentTimeMillis();
try {
Thread.sleep(0);
} catch (InterruptedException e) {
log.error("Interrupted", e);
}
}
}
那为什么设计者们不使用Thread.sleep()
而是调用Thread.sleep(0)
方法呢?原因如下:
不能不说RocketMQ的设计们对于编码的功力是非常深厚的。
到此为止,我们了解的操作系统对于CPU执行线程任务的调度流程,回到我们文章开头提出的几个问题:
1. 为什么并发的IO任务使用多线程效率更高?
答:IO阻塞的任务会让出CPU时间片,自行处理IO请求,确保操作系统尽可能榨取CPU利用率。
2. CPU在任务IO阻塞时发生了什么?
答:将任务放入等待队列,并切换到下一个要执行的线程中。
3. CPU切换线程的依据是什么?
答:有可能是分配给线程的时间片到期了,有可能是因为线程阻塞,还有可能因为线程霸占CPU太久了(针对抢占式算法)
4. 线程休眠有什么用?
答:以抢占式算法为例,线程休眠会将当前任务存入等待队列,并让CPU重新计算任务优先级,选出当前最高优先级的任务。
5. 线程休眠1秒后是否会立刻拿到CPU执行权。
答:不一定,CPU会按照调度算法执行任务,这个不能一概而论。
6. 为什么有人代码会用到`Thread.sleep(0);`它的作用是什么?
答:让当前线程让出CPU执行权,所有线程重新进行一次CPU竞争,优先级高的获取CPU执行权。
操作系统面试题:进程如何阻塞?进程阻塞为什么不占用CPU?:https://blog.csdn.net/weixin_44844089/article/details/115655642
Thread.sleep(0)并不是写错了,而是有妙用!:https://mp.weixin.qq.com/s/Zt8gnddY1fxFJpWT8mhWvA
面试题-Thread.sleep(0)的作用是什么:https://www.cnblogs.com/east7/p/14502400.html
没有二十年功力,写不出Thread.sleep(0)这一行“看似无用”的代码!:https://segmentfault.com/a/1190000042432589