写Java实验发现个有意思的问题
三个线程,一个线程打印字符a,一个线程打印字符b,另一个线程打印数字,多次运行结果都是先打印混合输出的ab,完了再打印数字
有图有真相,我运行了10次
完整的代码是这个
class PrintChar implements Runnable{
private char charToPrint;
private int times;
public PrintChar(char c,int t){
charToPrint=c;
times=t;
}
@Override
public void run() {
for(int i=0;i<times;i++){
System.out.print(charToPrint);
}
}
}
class PrintNum implements Runnable{
private int lastNum;
public PrintNum(int n){
lastNum=n;
}
@Override
public void run() {
for(int i=0;i<=lastNum;i++){
System.out.print(" "+i);
}
}
}
public class TaskThreadDemo {
public static void main(String[] args) {
Runnable printA=new PrintChar('a',100);
Runnable printB=new PrintChar('b',100);
Runnable print100=new PrintNum(100);
Thread thread1=new Thread(printA);
Thread thread2=new Thread(printB);
Thread thread3=new Thread(print100);
thread1.start();
thread2.start();
thread3.start();
}
}
字符a和字符b是混合输出的,这符合我们的预期,因为多线程是并发的,因此各个线程之间的输出顺序是不确定
但是我们却从中发现尽管字符a和b的顺序是不确定的,但是ab和数字的顺序却始终是先打印完ab再打印数字,这显然不科学,理论上数字也应该和ab一起混合输出,这究竟是为什么呢,我们观察到代码中,打印数字的线程是最后创建的,而且也是最后才启动的。
会不会是因为这个呢,于是我们改为最先创建打印数字的线程,最先启动打印数字的线程。
再次运行程序,很遗憾的发现,输出结果依然没有发生变化,数字依然在字母之后输出。
于是我们把注意力放到了线程本身进行比较,发现同样是打印,但是打印字母的是直接打印一个固定的字符变量,而打印数字的则是打印一个字符串和整型变量相加的结果。
我们把打印数字的换成一个固定的整形变量lastNum。
再次运行程序10次,结果如下,这次看到了字母ab和数字混合出现的结果,可见原因就出现在我们刚刚替换的代码处。
原本代码处是打印一个字符串和整型变量相加的结果,这里会隐形调用函数将整型变量转换为字符串,因此会比直接打印整型变量多一个函数调用的步骤,因此这里相比之下执行会更慢一些,而Java的线程调度是由操作系统内核来完成的,Java程序中的线程会被映射到操作系统的原生线程上,操作系统负责为这些线程分配CPU时间片,并根据调度策略来进行调度。那么在在默认情况下,Java线程的调度遵循抢占式的时间片轮转调度策略,每个线程都被分配一定的CPU时间片,当线程的时间片用完时,操作系统才会暂停该线程的执行,并将CPU时间片分配给其他等待执行的线程
所以这个CPU时间片足够让线程直接打印一个字符,但是不够让线程调用函数完成整型变量到字符串的转换以及相加的操作,因此在需要多个时间片完成打印数字的任务时,已经足够让打印字母的线程完成任务了。
为了验证我们的解释,我们将原本打印100个字母的线程任务换成了300个,让打印数字的线程有足够的CPU时间片在打印字母的线程还没完成任务的时候就打印出数字。
再次运行程序10次,此时出现了数字和字母混合输出的现象,说明我们的分析是对的。