线程通知与等待
Java中的Object类是所有类的父类,鉴于继承机制,Java把所有类都需要的方法放到了Object类里面,其中就包括了线程的通知和等待。
wait以及notify
当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起
。直到发生下面几件事情之一才返回。
- 其他线程调用了该共享对象的notify()或者notifyAll()。
- 其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。
另外,需要注意的是,如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()时调用线程会抛出IllegalMonitorStateException异常。
一个线程如何获取一个共享变量的监视器锁
- 方法1:执行sychronized同步代码块时,使用该共享变量作为参数
sychronized(该共享变量){
}
- 方法2:调用该共享变量的方法,并且该方法使用了sychronized修饰。
sychronized void method{
}
虚假唤醒:一个线程即使没有被其他线程调用notify()、notifyAll()进行通知或者被中断,等待超时也可以从挂起状态变为可以运行状态,这就是所谓的虚假唤醒。
虽然虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停的去测试该线程被唤醒的条件是否满足,不满足则继续等待。也就是说在一个循环中调用wait()方法进行防范,退出循环的条件是满足了唤醒该线程的条件。
synchronized (obj) {
while (条件不满足){
obj.wait() ;
}
}
实例
其中queue为共享变量,生产者线程在调用queue的wait()方法之前,使用sychronized关键字拿到了该共享变量queue的监视器锁,所以调用wait()方法才不会抛出IllegalMonitorStateException异常。如果当前队列没有空闲容量则会调用queued的wait()方法挂起当前线程,这里使用循环就是为了避免虚假唤醒问题。假如当前线程被虚假唤醒了,但是队列还是没有空余容量,那么当前线程还是会调用wait()方法把自己挂起。
//生产线程
synchronized (queue) {
//消费队列满,贝等待队列空闲
while (queue.size() == MAX SIZE) {
try {
//挂起当前线程, 并释放通过同步块获取的 queue上的锁,上消费者线程可以获取该锁,然后获取队列里面的元素
queue .wait() ;
} catch (Exception ex) {
ex.printStackTrace() ;
}
}
// 空闲则生成元素,并通知消费者线程
queue.add(e);
queue.notifyAll();
}
//消费者线程
sychronized(queue){
while(queue.size == 0){
try{
//挂起当前线程,并释放通过同步块获取的queue上的锁,让生产者线程可以获取该锁,将生产元素放入队列
queue.wait();
}catch(Exception e){
e.printStackTrace() ;
}
}
//消费元素,并唤醒生产者线程
queue.take();
queue.notifyAll();
}
此外,当前线程调用共享变量的wait()方法后,只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁不会释放。
如下所示:
//创建共享资源
private static volatile Object resourceA = new Object();
private static volatile Object resourceB = new Object();
new Thread(()->{
try{
sychronized(resourceA){
sychronized(resourceB){
// 此处线程A只释放了获取到的resourceA的锁,resourceB的锁并为释放
resourceA.wait();
}
}
}
}).start();
wait(long timeout)
该方法相比wait()多了一个超时参数,他的不同之处在于,如果一个线程调用共享对象的该方法挂起后,没有在指定的timeout ms时间内被其他线程调用该共享变量的notify()或者notifyAll()唤醒,该函数会因为超时而返回。如果将timeout设置为0,则不限制超时时间。
notify()
一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
此外,被唤醒的线程不能马上从wait方法返回并继续执行,他仍然需要和其他线程一起竞争该锁,只有竞争到了共享变量的监视器锁后才可以继续执行。
notifyAll()
不同于在共享变量上调用notify()函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll()方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。
等待线程执行终止的join方法
在主线程里面启动两个子线程,然后分别调用他们的join()方法,那么主线程首先会在调用theadOne.join()方法后被阻塞,等待theadOne执行完毕后返回。然后主线程调用threadTwo.join()方法后再次被阻塞,等待threadTwo执行完毕后返回。
yield()让出CPU执行权
Thread类中有一个静态的yield方法,当一个线程调用yield方法时,实际上就是在暗示线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示
。
当一个线程调用yield方法时,当前线程会让出CPU使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程。
线程中断
Java中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止线程的执行,而是被中断的线程根据中断状态自行处理。
-
void interrupt(): 中断线程,例如,当线程A运行时,
线程B可以调用线程A的interrupt()方法来设置线程A的中断标志为true
并立即返回。线程A实际并没有被中断
,他会继续往下执行。如果线程A因为调用了wait、join、sleep方法而被阻塞挂起,这个时候如果线程B调用线程A的interrupt方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返回。 -
boolean isInterrupted()方法: 检测当前线程是否被中断,是返回true。
-
boolean interrupted() 方法检测当前线程是否被中断,如果是返回true,否则返回false。与isInterrupted方法不同,该方法如果发现当前线程被中断,则会清除中断标志。
示例,当线程为了等待一些特定条件的到来时,一般会调用sleep函数、wait函数或者join函数来阻塞挂起当前线程。比如一个线程调用了Threa.sleep(3000), 那么调用线程会被阻塞3s后才会从阻塞状态变为激活状态。但是有时候,如果一直等到3s后再返回有点浪费时间。这时候可以 调用线程的interrupt()方法,强制sleep方法抛出InterruptedException异常而返回。线程恢复到活跃状态。
线程上下文切换
在多线程编程中,线程个数一般大于CPU个数,而每个CPU同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的,CPU资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU给其他线程占用,这就是上下文切换。
从当前线程的上下文切换到其他线程,被切换到的线程需要知道上次CPU运行到了哪里,所以在切换线程的时候要保存好上下文。当再次执行时根据保存的现场信息恢复执行。
守护线程和用户线程
Java中的线程分为两类,分别为daemon线程(守护线程)和user线程(用户线程)。在JVM启动时会调用main函数,main函数所在的线程就是一个用户线程,其实,在JVM内部同时还启动了好多守护线程,比如垃圾回收线程。
守护线程和用户线程有什么区别:区别之一是当最后一个非守护线程结束时,JVM会正常退出,而不管当前是否有守护线程,并不影响JVM的退出。
如何创建一个守护线程,代码如下
Thread thread = new Thread(() -> {
});
//设置为守护线程
thread.setDaemon(true);
thread.start();
只需要设置线程的dameon参数为true即可。
对比实验
例一:
public static void main(String[] args) {
new Thread(() -> {
for (; ; ) {
}
}).start();
System.out.println("main Thread is over");
}
结果表明,main线程运行结束,JVM进程并未退出,说明 当父线程结束后,子线程还是可以继续存在的,也就是子线程的生命周期并不受父线程的影响。也说明了在用户线程还存在的情况下JVM进程并不会终止。
例二:
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for (; ; ) {
}
});
thread1.setDameon(true);
thread1.start();
System.out.println("main Thread is over");
}
修改以后,发现当main线程运行结束以后,JVM进程也已经终止了,在这个例子中,main函数是唯一的用户线程,thread线程是守护线程,当main线程运行结束后,JVM发现当前已经没有用户线程了,就会终止JVM进程
。由于这里的守护线程执行的时候是一个死循环,这也说明了如果当前进程中不存在用户线程,但是还存在正在执行任务的守护线程,则 JVM不等守护线程运行完毕就结束JVM进程
。
总结:非守护线程(也称为用户线程)都结束了,JVM就会终止,此时所有守护线程会被强制终止,它们正在执行的任务也会随之中断
。
main线程运行结束后,JVM会自动启动一个叫做DestroyJavaVM的线程,该线程会等待所有用户线程结束后终止JVM进程。JVM代码如下:
int JNICALL
JavaMain(void * args){
//执行 Java中的 main函数
(*env) ->CallStaticVoidMethod(env, mainClass, mainID, mainArgs) ;
//main函数返回值
ret = (*env)->ExceptionOccured(env) == NULL ?0 :1;
//等待所有非守护线程结束, 然后销毁JVM进程
LEAVE();
}
LEAVE()是C语言中的一个宏定义,作用是创建一个名为DestroyJavaVM的线程来等待所有用户线程执行结束。
总结:如果希望在主线程结束后JVM进程马上结束,那么在创建线程时可以将其设置为守护线程,如果希望在主线程结束后子线程继续工作,等子线程结束后再让JVM进程结束,那么就将子线程设置为用户线程。
补充:执行kill 信号,杀死一个应用程序时,如果应用程序仍然有未完成的任务,可以通过添加钩子函数的方式使其可以继续处理并优雅退出
。
public class App {
public static void main(String[] args) {
// 添加一个关闭钩子
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("正在执行清理操作...");
// 这里放置清理资源的代码,例如关闭文件、数据库连接等
performCleanup();
System.out.println("清理完成。");
}));
System.out.println("应用程序正在运行...");
// 模拟应用程序运行
try {
Thread.sleep(10000); // 假设应用运行了一段时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("线程被中断。");
}
}
private static void performCleanup() {
// 执行实际的清理操作,比如关闭资源等
System.out.println("执行资源关闭等清理操作。");
}
}
本站资源均来自互联网,仅供研究学习,禁止违法使用和商用,产生法律纠纷本站概不负责!如果侵犯了您的权益请与我们联系!
转载请注明出处: 免费源码网-免费的源码资源网站 » Java并发编程之线程基础
发表评论 取消回复