线程的上下文切换
上下文:线程在执行过程中,线程自身的运行条件和状态,比如程序计数器、栈信息等。
线程退出RUNNING状态的情况:
- 主动让出CPU,如调用了
sleep()
、wait()
函数 - CPU时间片耗尽
- 调用了阻塞类型的系统中断,比如请求IO,线程被阻塞
- 被意外终止或结束运行
上述的四种情况中,前三中情况都会出现线程切换,线程切换意味着需要保存当前线程的上下文,待线程下次拥有CPU时间片时恢复现场,并加载下一个将要占用CPU的线程的上下文 => 上下文切换
上下文切换因为每次都需要保存和恢复信息,这将会占用CPU、内存等系统资源,也就意味着效率会有一定的损耗,如果频繁切换就会导致整体效率低下。
线程死锁
概念
多个线程同时被阻塞,他们中的一个或者全部都在等待某个资源被释放,由于线程被无限期的阻塞,因此程序不可能正常终止。
「线程死锁示意图」
死锁示例代码(Java)
public class DeadLockDemo { |
输出结果:
14:49:12.184 [Thread B] INFO com.juzi.juc.demo.DeadLockDemo - Thread B has resource 2 |
上述代码中,线程A通过synchorized(resource1)
获得了资源1resource1
的监视器锁,然后线程A休眠1s,这么做的目的是让线程B获取资源2resource2
的监视器锁。当两个线程休眠结束之后,都开始请求获取对象已经占有的资源,然后这两个线程就会陷入互相等待的状态,也就产生了死锁。
上述的例子符合死锁产生的四个必要条件:
- 互斥:资源任意一个时刻只由一个线程持有
- 请求和保持:一个线程因请求资源而阻塞时,对已获得的资源保持不释放
- 不可剥夺:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕之后才能释放资源
- 循环等待:若干线程之间形成头尾相接的循环等待资源关系
预防死锁
破坏死锁产生的必要条件即可。
- 破坏请求和保持条件:一次性申请所有的资源
- 破坏不可剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源
- 破坏循环等待条件:按顺序申请资源
避免死锁
在资源分配时,借助算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
安全状态:系统能够按照某种线程推进顺序来为每个线程分配所需的资源,知道满足各个线程对资源的最大需求,使每个线程都可以顺利完成,那么这个顺序的序列就是安全序列。
sleep()
和wait()
对比
同:两个方法都可以暂停线程的执行
异:
- 锁释放角度:
sleep()
方法没有释放占有的锁资源,wait()
方法会释放锁 - 应用场景:
sleep()
方法通常被用于暂停执行,wait()
方法通常用于线程间交互、通信 - 是否苏醒角度:调用
wait()
方法后,线程不会自动苏醒,需要其他线程调用同一对象上的notify()
或者notifyAll()
方法,调用wait(long timeout)
,超时后线程会自动苏醒;sleep()
方法执行完成后,线程会自动苏醒 - 方法位置:
sleep()
方法是Thread
类的静态本地方法,wait()
则是Object
类的本地方法
wait()
方法定义在Object中的原因
wait()
方法的作用是让获得对象锁的线程实现等待,这个过程会自动释放当前线程占用的对象锁。每个Object对象都拥有对象锁,要释放当前线程占有的对象锁,操作的对象是Object
而不是当前线程(Thread
)。
sleep()
方法定义在Thread
类中的原因:因为
sleep()
方法是让当前线程暂停运行,这个过程并不涉及对象类,也不需要释放对象锁,
调用Thread类的run()
方法会启动一个新线程吗?
真正开始多线程工作的流程:创建一个Thread
,此时线程处理NEW状态,调用start()
方法,会启动一个线程并且使线程进入RUNNABLE状态,当分配了CPU时间片就可以执行了。这个过程中,start()
方法会执行相应的准备工作,然后自动执行run()
方法的内容。
显然,直接执行run()
方法,会把run()
方法当成一个main线程下普通方法去执行,并不会新起一个线程去执行它。
参考文章
- https://javaguide.cn/java/concurrent/java-concurrent-questions-01.html