当前位置: 首页 > 科技观察

闲鱼这边:Thread.sleep(0)有什么用?

时间:2023-03-20 20:00:51 科技观察

思考下面两个问题:假设是2008-4-712:00:00.000,如果我调用Thread.Sleep(1000),在2008-4-712:00:01.000,这个线程会不会被吵醒?有人的代码用了一句看似莫名其妙的代码:Thread.Sleep(0)。既然是Sleep0毫秒,那他和去掉这段代码有什么区别吗?我们先回顾一下操作系统原理。在操作系统中,竞争CPU的策略有很多。Unix系统采用的是时间片算法,而Windows是抢占式的。在时间片算法中,所有进程都排成一个队列。操作系统按照它们的顺序为每个进程分配允许该进程运行的时间段。如果时间片结束时进程还在运行,CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU立即切换。调度器所要做的就是维护一个就绪进程列表,当一个进程用完它的时间片时,它会被移到队列的末尾。所谓抢占式操作系统,是指如果一个进程获得了CPU时间,它将完全占用CPU,除非它自己放弃使用CPU。因此可以看出,在抢占式操作系统中,操作系统假定所有进程都是“好人品”,会主动退出CPU。在抢占式操作系统中,假设有多个进程,操作系统会根据它们的优先级和饥饿时间(CPU多长时间没有被使用)计算出它们的总优先级。操作系统会将CPU分配给总体优先级最高的进程。当进程执行完毕或自己挂起时,操作系统会重新计算所有进程的总优先级,然后挑选出优先级最高的进程,将CPU控制权交给它。我们以分蛋糕的场景来描述这两种算法。假设有源源不断的蛋糕(连续时间),一把刀叉(一个CPU),10个人在等着吃蛋糕(10个进程)。如果是Unix操作系统负责分蛋糕,那么他会制定这样一个规则:每个人上来吃1分钟,时间到了再换下一个。当最后一个人吃完后,重新开始。所以,不管这10个人的优先级不同,饥饿程度不同,胃口不同,每个人上来都能吃1分钟。当然,如果有人不是太饿或者吃的很少,30秒后就饱了,他可以告诉操作系统:我吃饱了(挂起)。所以操作系统会让下一个人跟随。如果Windows操作系统负责分蛋糕,那场景就很有趣了。他会制定这样的规则:我会根据你们的优先级和饥饿程度,为你们每个人计算一个优先级。优先级最高的人可以上来吃蛋糕——直到你不想吃为止。这个人吃完之后,我会根据优先级和饥饿程度重新计算每个人的优先级,然后分配给优先级最高的人。这样一来,这一幕就有意思了——可能有人是PPMM,所以优先级高,这样她才能经常过来吃蛋糕。可能对方是个丑男,走的很ws,所以优先级很低,所以他转了一次就花了很长时间(因为随着时间的推移,他会越来越饿,所以计算出来的总的优先级会越来越高,总有一天会轮到他的)。而且,如果一个大胖子不小心拿到了刀叉,因为他吃的多,可能会霸占蛋糕吃很久,导致旁边的人在那里咽下去。..而且,还可能出现这样一种情况:现在操作系统计算出来的结果,总优先级5号的PPMM最高,比其他的高很多。于是叫5号吃蛋糕。5日吃了一会,觉得不那么饿了,就说“我不吃”(挂)。最新的多线程面试题已经整理完毕,大家可以在Java面试库小程序中在线刷题。所以操作系统重新计算每个人的优先级。因为5号刚刚吃过饭,她的饥饿度变小了,所以总的优先级变小了;而因为其他人等了一段时间,饥饿度变大了,所以总的优先级也变高了。不过,此时五号的优先级还是有可能比其他人高的,只是现在只比其他人高了那么一点而已——但她依然是总优先级最高的。于是操作系统就会说:MM5来吃蛋糕了……(MM5郁闷啊,她不就是吃了吗……她要减肥……谁告诉你你这么漂亮又这么漂亮获得如此高的优先级)。那么,Thread.Sleep函数有什么作用呢?也用刚才分蛋糕的场景来形容。上图中,5号MM吃完一次蛋糕就觉得80%饱了,她觉得接下来的半个小时不想再吃蛋糕了,所以她会告诉操作系统:不要给我打电话下半小时吃蛋糕。这样,当操作系统在接下来的半小时内重新计算每个人的总优先级时,就会忽略第5个mm。睡眠功能就是这样做的。他告诉操作系统“我不会在接下来的几毫秒内参与CPU竞争”。看完了Thread.Sleep的功能,我们来思考一下文章开头的两个问题。对于第一个问题,答案是:不一定。因为你只是告诉操作系统:我不想在接下来的1000毫秒内参与CPU竞争。那么1000毫秒后,此时可能有另一个线程正在使用CPU,那么操作系统不会重新分配CPU,直到该线程被挂起或终止;而且,即使此时恰好轮到操作系统分配CPU,那么当前线程也不一定是总优先级最高的线程,CPU仍有可能被其他线程抢占。与此类似,Thread有一个Resume函数,用于唤醒挂起的线程。上面说了,这个函数只是“告诉操作系统,从现在开始我要参加CPU竞争”,这个函数的调用并不会立即让这个线程获得CPU控制权。对于第二个问题,答案是:是的,而且区别很明显。假设刚才我们分蛋糕的场景,还有一个7号PPMM,她的优先级也非常非常高(因为她非常非常漂亮),所以操作系统会一直叫她吃蛋糕.另外整理了Java多线程系列的所有面试题和答案,大家可以使用Java面试库小程序在线写题。而且7号也很喜欢吃蛋糕,胃口也很大。不过7号的人品很好,很善良,吃了几口就会想:如果别人比我更需要蛋糕,那我就给他。所以她可以每隔几口就对操作系统说:让我们重新计算每个人的总体优先级。但是,操作系统不接受这个建议——因为操作系统不提供这个接口。于是7号改口说:“以后0毫秒内不要叫我上来吃蛋糕。”操作系统接受了这个指令,所以此时操作系统会重新计算每个人的总体优先级——注意这次是和数字7一起计算的,因为“0毫秒过去了”。所以,如果没有人比7号更需要蛋糕,那么下次还是会叫7号来吃蛋糕。所以Thread.Sleep(0)的作用是“触发操作系统立即重启CPU竞争”。竞争的结果可能是当前线程仍然获得CPU控制权,也可能被其他线程代替获得CPU控制权。这就是为什么我们经常在大循环中写Thread.Sleep(0),因为这给了Paint线程等其他线程获得CPU控制权的权力,这样界面就不会卡在那里。另外,虽然上面说了“除非它自己放弃使用CPU,否则它会完全占用CPU”,这个行为还是有限制的——操作系统会监控你的CPU占用情况,如果发现有线程占用CPU长时间占用CPU会强制线程挂起,所以实际上不会出现“一个线程一直占用CPU”的情况。至于我们的大循环导致程序卡死,并不是因为这个线程一直在占用CPU。其实这段时间操作系统已经多次竞争CPU,但是其他线程获得CPU控制权后马上就退出了,所以轮到这个线程继续执行循环,所以又花了很长时间操作系统强制挂起的时间。..所以反映在界面上,好像这个线程一直在占用CPU。最后说明一下,本文中线程和进程有点混淆。其实在Windows原理层面,CPU的竞争是线程层面的。在这篇文章中,把这里的进程和线程看成一个东西就好了。