1.线程的生命周期及状态转换
在Java中任何对象都有自己的生命周期,线程也是一样。
线程生命周期的开始: Thread对象在创建完成后,线程的生命周期便开始了。
线程生命周期的结束: 在程序代码正常执行完毕或者线程抛出一个未捕获的异常(Exception)或者错误(Error)时,线程生命周期的就会结束了。。
线程的状态: Java中将线程的生命周期分为6个状态,分别是NEW(新建状态),RUNNABLE(可运行状态),BLOCKED(阻塞状态),WAITING(等待状态),TIMED_WAITING(定时等待状态)和TERMINATED(中止状态)。线程的不同状态表明了线程当前正在进行的活动。在程序中通过一些操作,可以使线程在不同的状态之间转换。如下图:
1.1 NEW(新建状态)
创建一个线程后,该线程处于新建状态,此时他不能运行,和其他的Java对象一样,仅仅为其分配了内存,没有表现出任何线程的动态特征。。
1.2 RUNNABLE(可运行状态)
在新建状态下的线程对象调用了start()方法,此时就会从新建状态进入可运行的状态,在RUNNABLE状态内部又细分为两种状态:READY(就绪状态)和RUNNABLE(运行状态),并且线程可以在两种状态之间进行转换。
- ① 就绪状态:线程对象调用start()方法之后,等待JVM的调度,此时线程并没有运行。
- ② 运行状态:线程对象获得JVM的调度,如果存在多个CPU,那么允许多个线程并行运行。
1.3 BLOCKED(阻塞状态)
处于运行状态的线程可能会因为某些原因失去CPU的执行权,暂时停止运行进入阻塞状态。此时, JVM不会给线程分配CPU,直到线程重新进入就绪状,才有机会转换到运行状态。阻塞状态的线程只能先进入就绪状态,不能直接进入运行状态。
线程一般会在以下两种情况下进入阻塞状态:
- ① 当线程A运行过程中,试图获取同步锁时,却被线程B获取,此时JVM把当前线程A存到对象的锁池中,线程A就会进入阻塞状态。
- ② 当线程运行过程中,发出I/O请求,此时该线程也会进入阻塞状态。
1.4 WAITING(等待状态)
当处于运行状态的线程调用了无时间参数限制的方法后,如wait(),join()等方法,就会将当前运行中的线程转换为等待状态。
处于等待状态中的线程不能立即争夺CPU使用权,必须等待其他线程执行特定的操作后,才有机会再次争夺CPU使用权,将等待状态的线程转换为运行状态。例如,调用wait()方法而处于等待状态中的线程,必须等待其他线程调用notify()或者notifyAll()方法唤醒当前等待中的线程,调用join()方法而处于等待状态中的线程,必须等待其他加入的线程终止。。
1.5 TIMED_WAITING(定时等待状态)
将运行状态中的线程转换为定时等待状态中的线程,与转换为等待状态中的线程操作类似,只有运行线程调用了有时间参数限制的方法,如sleep(long millis),wait(long timeout),join(long millis)等方法。
处于定时等待状态中的线程也不能立即争夺CPU的使用权,必须等待其他相关线程执行完特定的操作或者限时时间结束后,才有机会再次争夺CPU的使用权,将定时等待状态的线程转换为运行状态。例如,调用了wait(long timeout)方法而处于等待状态中的线程,需要通过其他线程调用notify()或者notifyAll()方法唤醒当前等待状态中的线程,或者等待限时时间结束后也可以进行状态转换。
1.6 TERMINATED(中止状态)
线程的run()方法,call()方法正常执行完毕或者线程抛出一个未捕获的异常,错误(Error),线程就进入中止状态。一旦进入中止状态,线程将不再有运行的资格,也不能再转换为其他状态,生命周期结束。。
2.线程的调度
程序中的多个线程是并发执行的,但并不是在同一时刻执行,某个线程若想被执行必须要获得CPU的使用权。Java虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制被称为线程的调度。
线程调度的两种模型:
- ① 分时调度模型:让所有的线程轮流获取CPU的使用权,并且平均分配每个线程占用的CPU时间片。
- ② 抢占式调度模型:让可运行池中所有就绪状态的线程争抢CPU的使用权,而优先级高的线程获取CPU执行权的概率大于优先级低的线程。
Java虚拟机默认采用的是抢占式调度模型。 在大多数情况下,程序员不需要关心它,但是在特定的情况下需要改变这种模式,由程序员自己控制CPU的调度。
2.1 线程的优先级
在程序中,如果想要改变线程进行调度,最直接的方式是设置线程的优先级。优先级越高,获取CPU执行的机会越大,而优先级越低则获取CPU执行的机会越小。
设置优先级的两种方法:
通过setPriority(int newPriority)进行设置,参数newPriority接受的是1-10之间的整数或者Thread类的三个静态常量。
- ① 使用数字设置优先级: 线程的优先级用1-10之间的整数来表示,数字越大优先级越高。
- ② 使用静态常量:使用Thread中提供的三个静态常量来表示线程的优先级,如下图表示:
Thread类中的静态常量 | 功能描述 |
---|---|
public final static int MIN_PRIORITY = 1 | 表示线程的最低优先级 |
public final static int NORM_PRIORITY = 5 | 表示线程的普通优先级 |
public final static int MAX_PRIORITY = 10 | 表示线程的最高优先级 |
程序在运行期间,处于就绪状态的每个线程都有自己的优先级,例如main()线程具有普通优先级。
public class test {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "正在输出i:" + i);
}
}, "优先级较低的线程");
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "正在输出i:" + i);
}
}, "优先级较高的线程");
//分别设置优先级
thread1.setPriority(Thread.MIN_PRIORITY);
thread2.setPriority(10);
thread1.start();
thread2.start();
}
}
注: 虽然Java中提供了10个线程优先级,但是这些优先级需要操作系统的支持,不同的操作系统对优先级的支持是不一样的,不能很好的和java中线程优先级一一对应。因此,在设计多线程的应用中,其功能一定不能依赖于线程的优先级,而只能将线程优先级当作一种提高程序效率的手段。
2.2 线程休眠
如果想人为的控制线程的执行顺序,使正在运行的线程暂停,将CPU使用权让给其他线程,这时就可以使用静态方法sleep(long millis),该方法可以让正在执行的线程暂停一段时间,进入休眠等待状态,这样其他线程就可以得到执行的机会。sleep(long millis)方法抛出InterruptedException异常,因此在调用该方法时应该捕获异常,或者申明抛出异常。
public class test {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "正在输出i:" + i);
if(i==2){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "优先级较低的线程");
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "正在输出i:" + i);
}
}, "优先级较高的线程");
thread1.start();
thread2.start();
}
}
注: 线程类Thread提供了两种线程休眠的方法,sleep(long millis)和sleep(long millis,int nanos),这两种方法都带有休眠时间参数,当其他线程都终止后并不代表当前休眠的线程会立即执行,而是必须当休眠时间结束后,线程才会转换到就绪状态。
2.3 线程让步
线程让步可以通过yield()方法来实现,该方法和sleep(long millis)方法有点类似,都可以让当前正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,它只是将线程状态转换为就绪状态,让系统的调度器重新调度一次。
class myThread extends Thread{
public myThread(String name) {
super(name);
}
@Override
public void run() {
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"----"+i);
if(i==2){
System.out.println("线程让步:");
Thread.yield();
}
}
}
}
public class test {
public static void main(String[] args) {
myThread myThread1 = new myThread("Thread1");
myThread myThread2 = new myThread("Thread2");
myThread1.start();
myThread2.start();
}
}
注: 通过yield()方法实现线程让步,让当前正在运行的线程失去CPU的使用权,让系统的调度器重新调度一次,由于Java虚拟机默认的调度机制是抢占式调度模型,所有的线程都会再次抢占CPU资源使用权,所以在执行线程让步后并不能保证立即执行其他线程。
2.4 线程插队
现实生活中,经常会遇到"插队"的情况,在Thread类中也提供一个join()方法用来实现这个功能。当这个线程中调用其他线程的join()方法是,调用的线程将会被阻塞,直到被join()方法加入的线程执行完毕后才会继续执行。
class myThread implements Runnable{
@Override
public void run() {
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"----"+i);
}
}
}
public class test {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new myThread(), "thread1");
thread1.start();
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"----"+i);
if(i==2){
thread1.join();
}
}
}
}
Thread类中除了提供了一个无参数的线程join()方法外,还提供了带有时间参数的线程插队方法join(long millis)。当执行带有时间参数的join(long millis)方法进行线程插队时,必须等待插入的线程指定时间过后才能继续执行其他的线程。。