计算机通常只有一个CPU。在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才可以执行指令。所谓线程并发运行,从宏观上看,就是各个线程轮流获得CPU使用权,分别执行各自的任务。在可运行池中,会有多个处于就绪状态的线程在等待CPU,Java虚拟机的一项任务就是负责线程的调度。线程的调度是指按照特定的机制为多个线程分配CPU的使用权,有两种调度模型:分时调度模型和抢占式调度模型。
分时调度模型是指让所有线程轮流获得CPU的使用权,并且平均分配每个线程占用CPU的时间片。
Java虚拟机采用的是抢占式调度模型,它是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中线程的优先级相同,那么就随机的选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。一个线程会因为一下原因放弃CPU:
Java虚拟机让当前线程暂时放弃CPU,转到就绪状态,使其他线程获取运行机会。
当前线程因为某些原因进入阻塞状态
线程运行结束
线程的调度规则不是跨平台的,它不仅取决于Java虚拟机,还依赖于操作系统。在某些操作系统中,只要运行的线程没有遇到阻塞,就不会放弃CPU;在某些操作系统中,即使运行中的线程没有遇到阻塞,也会在运行一段时间后放弃CPU,给其他线程机会。
下面的代码可以证明Java线程调度不是分时的,不能保证各个线程轮流获得CPU时间片:
public class Machine extends Thread{
private static StringBuffer log=new StringBuffer();
private static int count=0;
public void run() {
for(int a=0;a<20;a++){
log.append(currentThread().getName()+":"+a+" ");
if(++count%10==0)log.append("\n");
}
}
public static void main(String[] args) throws InterruptedException {
Machine machine1=new Machine();
Machine machine2=new Machine();
machine1.setName("m1");
machine2.setName("m2");
machine1.start();
machine2.start();
while(machine1.isAlive()||machine2.isAlive())
Thread.sleep(500);
System.out.println(log);
}
}
打印结果:
上面的打印结果,表明machine1线程先获得CPU,进入运行状态,直到machine1线程结束生命周期,machine2线程才从就绪状态转到运行状态。
如果希望machine1给machine2运行的机会,可以采取以下办法:
调整线程的优先级
让处于运行状态的线程调用Thread.sleep()方法
让处于运行状态的线程调用Thread.yield()方法
让处于运行状态的线程调用另一个线程的join()方法
调整线程的优先级
所有处于就绪状态的线程根据优先级存放在可运行池中,优先级低的线程获得较少的运行机会,优先级高的线程获得较高的运行机会。Thread类的setPriority(int)和getPriority()分别用来设置优先级和读取优先级。优先级用整数表示,取值范围是1~10,Thread类有3个静态常量:
MAX_PRIORITY:取值为10,表示最高优先级
MIN_PRIORITY:取值为1,表示最低优先级
NORM_PRIORITY:取值为5,表示默认的优先级
我们对第一个示例做该造,分别设置machine1和machine2的优先级:
public static void main(String[] args) throws InterruptedException {
Machine machine1=new Machine();
Machine machine2=new Machine();
machine1.setName("m1");
machine2.setName("m2");
/*查看和设置线程的优先级*/
//打印线程的默认优先级
System.out.println(machine1.getPriority());
System.out.println(machine2.getPriority());
machine2.setPriority(Thread.MAX_PRIORITY);
machine1.setPriority(Thread.MIN_PRIORITY);
machine1.start();
machine2.start();
Thread.sleep(2000);
System.out.println(log);
}
上面代码machine2的优先级高于machine1,程序运行的可能结果:
每个线程都有默认的优先级,主线程默认优先级是Thread.NORM_PRIORITY。如果线程A创建了线程B,那么线程B将和线程A具有同样的优先级。当然,对于任意一个线程,都可以通过setPriority()方法重新设置它的优先级。
需要注意的是,尽管Java提供了10个优先级,但是它与多数操作系统都不能很好地进行线程优先级映射。比如Windows有7个优先级,并且不是固定的,而Solaris操作系统有2的31次方个优先级。如果希望程序能够移植到各个操作系统中,就应该确保在设置优先级时候,只用MAX_PRIORITY、NORM_PRIORITY、MIN_PRIORITY这3个级别,以保证在不同操作系统中,对同样优先级别的线程采用同样的调度方式。
线程睡眠Thread.sleep()方法
当一个线程在运行中执行了sleep()方法,它就会放弃CPU,转到阻塞状态。
Thread类的sleep(long mills)方法是静态的,mills参数设定睡眠的时间,以毫秒为单位。假定一个线程对象在某一时刻获得CPU,开始执行指令,当它执行到sleep()方法时,就会放弃CPU并开始睡眠。等到结束睡眠,首先转到就绪状态,不一定会立即执行,而是在可运行池中等待获得CPU。
线程在睡眠时被中断,会抛出一个InterrupedException异常,请看下面示例:
public class Sleeper extends Thread{
@Override
public void run() {
try {
sleep(60000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end");
}
public static void main(String[] args) throws InterruptedException {
Sleeper sleeper=new Sleeper();
sleeper.start();
Thread.sleep(10);
sleeper.interrupt();//中断Sleeper线程的睡眠
}
}
主线程启动Sleeper线程,Sleeper线程睡眠1分钟,主线程10秒后中断Sleeper线程的睡眠,程序打印结果:
线程让步:Thread.yield()方法
当线程在运行中执行了Thread的yield()静态方法时,如果此时有处于相同优先级的其他线程处于就绪状态,yield()方法就会使当前运行的线程放到可运行池中,并使另一个线程运行。如果没有相同优先级的可运行线程,yield()方法什么都不做。
sleep()方法和yield()方法都是Thread类的静态方法,都会使当前处于运行状态的线程放弃CPU,把运行机会让给其他线程,两者区别在于:
sleep()方法会给其他线程运行的机会,不考虑其他线程的优先级,因此会给较低优先级线程一个运行机会;yield()方法只会给相同优先级或更高优先级的线程一个运行的机会。
当线程执行了sleep(long mills)方法后,将转到阻塞状态,参数mills指定睡眠时间;当线程执行了yield()方法以后,将转到就绪状态
sleep()方法声明抛出InterruptedException异常,而yield()方法没有声明抛出任何异常
sleep()方法比yield()方法有更好的可移植性。不能依靠yield()方法来提高程序的并发性能。对于大多数程序员来说,yield()方法唯一用途是在测试期间人为的提高程序的并发性能,以帮助发现一些隐藏的错误。
等待其他线程结束:join()
当前运行的线程可以调用另一个线程的join()方法,当前线程将转入阻塞状态,直到另一个线程运行结束,它才会转到就绪状态。
public class Machine extends Thread{
public void run() {
for(int a=0;a<50;a++)
System.out.println(getName()+":"+a);
}
public static void main(String[] args) throws InterruptedException {
Machine machine1=new Machine();
machine1.setName("m1");
machine1.start();
System.out.println("main join machine");
machine1.join();
System.out.println("main end");
}
}
上面示例代码,主线程调用了machien1线程的join方法,主线程将等到machine1线程运行结束以后,才恢转到就绪状态,打印结果:
main join machine
m1:0
m1:1
...
m1:49
main end
join()方法有两种重载形式:
public void join()
public void join(long timeout)
上面的timeout参数设定当前线程被阻塞时间,以毫秒为单位。如果把上面示例代码的"machine1.join()"改为
machine1.join(10);
那主线程被阻塞的时间超过了10毫秒时,或者machine1线程运行结束时,主线程就转到就绪状态。