3.5 线程的基本方法
线程相关的基本方法有wait、notify、notifyAll、sleep、join、yield等,这些方法控制线程的运行,并影响线程的状态变化。
3.5.1 线程等待:wait方法
调用wait方法的线程会进入WAITING状态,只有等到其他线程的通知或被中断后才会返回。需要注意的是,在调用wait方法后会释放对象的锁,因此wait方法一般被用于同步方法或同步代码块中。
3.5.2 线程睡眠:sleep方法
调用sleep方法会导致当前线程休眠。与wait方法不同的是,sleep方法不会释放当前占有的锁,会导致线程进入TIMED-WATING状态,而wait方法会导致当前线程进入WATING状态。
3.5.3 线程让步:yield方法
调用yield方法会使当前线程让出(释放)CPU执行时间片,与其他线程一起重新竞争CPU时间片。在一般情况下,优先级高的线程更有可能竞争到CPU时间片,但这不是绝对的,有的操作系统对线程的优先级并不敏感。
3.5.4 线程中断:interrupt方法
interrupt方法用于向线程发行一个终止通知信号,会影响该线程内部的一个中断标识位,这个线程本身并不会因为调用了interrupt方法而改变状态(阻塞、终止等)。状态的具体变化需要等待接收到中断标识的程序的最终处理结果来判定。对interrupt方法的理解需要注意以下4个核心点。
◎ 调用interrupt方法并不会中断一个正在运行的线程,也就是说处于Running状态的线程并不会因为被中断而终止,仅仅改变了内部维护的中断标识位而已。具体的JDK源码如下:
public static boolean interrupted() { return currentThread().isInterrupted(true); } public boolean isInterrupted() { return isInterrupted(false); }
◎ 若因为调用sleep方法而使线程处于TIMED-WATING状态,则这时调用interrupt方法会抛出InterruptedException,使线程提前结束TIMED-WATING状态。
◎ 许多声明抛出InterruptedException的方法如Thread.sleep(long mills),在抛出异常前都会清除中断标识位,所以在抛出异常后调用isInterrupted方法将会返回false。
◎ 中断状态是线程固有的一个标识位,可以通过此标识位安全终止线程。比如,在想终止一个线程时,可以先调用该线程的interrupt方法,然后在线程的run方法中根据该线程isInterrupted方法的返回状态值安全终止线程。
public class SafeInterruptThread extends Thread { @Override public void run() { if (! Thread.currentThread().isInterrupted()) { try { //1:这里处理正常的线程业务逻辑 sleep(10); } catch (InterruptedException e) { Thread.currentThread().interrupt(); //重新设置中断标识 } } if (Thread.currentThread().isInterrupted()){ //2:处理线程结束前必要的一些资源释放和清理工作,比如释放锁、 //存储数据到持久化层、发出异常通知等,用于实现线程的安全退出 sleep(10); } } } //3:定义一个可安全退出的线程 SafeInterruptThread thread = new SafeInterruptThread(); //4:安全退出线程 thread.interrupt();
3.5.5 线程加入:join方法
join方法用于等待其他线程终止,如果在当前线程中调用一个线程的join方法,则当前线程转为阻塞状态,等到另一个线程结束,当前线程再由阻塞状态转为就绪状态,等待获取CPU的使用权。在很多情况下,主线程生成并启动了子线程,需要等到子线程返回结果并收集和处理再退出,这时就要用到join方法,具体的使用方法如下:
System.out.println("子线程运行开始!"); ChildThread childThread = new ChildThread(); childThread.join(); //等待子线程childThread执行结束 System.out.println("子线join()结束,开始运行主线程");
3.5.6 线程唤醒:notify方法
Object类有个notify方法,用于唤醒在此对象监视器上等待的一个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的。
我们通常调用其中一个对象的wait方法在对象的监视器上等待,直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他线程竞争。类似的方法还有notifyAll,用于唤醒在监视器上等待的所有线程。
3.5.7 后台守护线程:setDaemon方法
setDaemon方法用于定义一个守护线程,也叫作“服务线程”,该线程是后台线程,有一个特性,即为用户线程提供公共服务,在没有用户线程可服务时会自动离开。
守护线程的优先级较低,用于为系统中的其他对象和线程提供服务。将一个用户线程设置为守护线程的方法是在线程对象创建之前用线程对象的setDaemon(true)来设置。
在后台守护线程中定义的线程也是后台守护线程。后台守护线程是JVM级别的,比如垃圾回收线程就是一个经典的守护线程,在我们的程序中不再有任何线程运行时,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以在回收JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态下运行,用于实时监控和管理系统中的可回收资源。
守护线程是运行在后台的一种特殊线程,独立于控制终端并且周期性地执行某种任务或等待处理某些已发生的事件。也就是说,守护线程不依赖于终端,但是依赖于JVM,与JVM“同生共死”。在JVM中的所有线程都是守护线程时,JVM就可以退出了,如果还有一个或一个以上的非守护线程,则JVM不会退出。
至此,对影响线程的核心方法基本介绍完毕,各方法对线程状态的影响如图3-5所示。
图3-5
3.5.8 sleep方法与wait方法的区别
sleep方法与wait方法的区别如下。
◎ sleep方法属于Thread类,wait方法则属于Object类。
◎ sleep方法暂停执行指定的时间,让出CPU给其他线程,但其监控状态依然保持,在指定的时间过后又会自动恢复运行状态。
◎ 在调用sleep方法的过程中,线程不会释放对象锁。
◎ 在调用wait方法时,线程会放弃对象锁,进入等待此对象的等待锁池,只有针对此对象调用notify方法后,该线程才能进入对象锁池准备获取对象锁,并进入运行状态。
3.5.9 start方法与run方法的区别
start方法与run方法的区别如下。
◎ start方法用于启动线程,真正实现了多线程运行。在调用了线程的start方法后,线程会在后台执行,无须等待run方法体的代码执行完毕,就可以继续执行下面的代码。
◎ 在通过调用Thread类的start方法启动一个线程时,此线程处于就绪状态,并没有运行。
◎ run方法也叫作线程体,包含了要执行的线程的逻辑代码,在调用run方法后,线程就进入运行状态,开始运行run方法中的代码。在run方法运行结束后,该线程终止,CPU再调度其他线程。
3.5.10 终止线程的4种方式
1.正常运行结束
指线程体执行完成,线程自动结束。
2.使用退出标志退出线程
在一般情况下,在run方法执行完毕时,线程会正常结束。然而,有些线程是后台线程,需要长时间运行,只有在系统满足某些特殊条件后,才能触发关闭这些线程。这时可以使用一个变量来控制循环,比如设置一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出,具体的实现代码如下:
public class ThreadSafe extends Thread { public volatile boolean exit = false; public void run() { while (! exit){ //执行业务逻辑代码 } } }
以上代码在线程中定义了一个退出标志exit, exit的默认值为false。在定义exit时使用了一个Java关键字volatile,这个关键字用于使exit线程同步安全,也就是说在同一时刻只能有一个线程修改exit的值,在exit为true时,while循环退出。
3.使用Interrupt方法终止线程
使用interrupt方法终止线程有以下两种情况。
(1)线程处于阻塞状态。例如,在使用了sleep、调用锁的wait或者调用socket的receiver、accept等方法时,会使线程处于阻塞状态。在调用线程的interrupt方法时,会抛出InterruptException异常。我们通过代码捕获该异常,然后通过break跳出状态检测循环,可以有机会结束这个线程的执行。通常很多人认为只要调用interrupt方法就会结束线程,这实际上理解有误,一定要先捕获InterruptedException异常再通过break跳出循环,才能正常结束run方法。具体的实现代码如下:
public class ThreadSafe extends Thread { public void run() { while (! isInterrupted()){ //在非阻塞过程中通过判断中断标志来退出 try{ Thread.sleep(5*1000); //在阻塞过程中捕获中断异常来退出 }catch(InterruptedException e){ e.printStackTrace(); break; //在捕获到异常后执行break跳出循环 } } } }
(2)线程未处于阻塞状态。此时,使用isInterrupted方法判断线程的中断标志来退出循环。在调用interrupt方法时,中断标志会被设置为true,并不能立刻退出线程,而是执行线程终止前的资源释放操作,等待资源释放完毕后退出该线程。
4.使用stop方法终止线程:不安全
在程序中可以直接调用Thread.stop方法强行终止线程,但这是很危险的,就像突然关闭计算机的电源,而不是正常关机一样,可能会产生不可预料的后果。
在程序使用Thread.stop方法终止线程时,该线程的子线程会抛出ThreadDeatherror错误,并且释放子线程持有的所有锁。加锁的代码块一般被用于保护数据的一致性,如果在调用Thread.stop方法后导致该线程所持有的所有锁突然释放而使锁资源不可控制,被保护的数据就可能出现不一致的情况,其他线程在使用这些被破坏的数据时,有可能使程序运行错误。因此,并不推荐采用这种方法终止线程。