youyichannel

志于道,据于德,依于仁,游于艺!

0%

JUC知识点_02

线程的上下文切换

上下文:线程在执行过程中,线程自身的运行条件和状态,比如程序计数器、栈信息等。

线程退出RUNNING状态的情况:

  • 主动让出CPU,如调用了sleep()wait()函数
  • CPU时间片耗尽
  • 调用了阻塞类型的系统中断,比如请求IO,线程被阻塞
  • 被意外终止或结束运行

上述的四种情况中,前三中情况都会出现线程切换,线程切换意味着需要保存当前线程的上下文,待线程下次拥有CPU时间片时恢复现场,并加载下一个将要占用CPU的线程的上下文 => 上下文切换

上下文切换因为每次都需要保存和恢复信息,这将会占用CPU、内存等系统资源,也就意味着效率会有一定的损耗,如果频繁切换就会导致整体效率低下。

线程死锁

概念

多个线程同时被阻塞,他们中的一个或者全部都在等待某个资源被释放,由于线程被无限期的阻塞,因此程序不可能正常终止。

「线程死锁示意图」

死锁示例代码(Java)

public class DeadLockDemo {

private static final Logger logger = LoggerFactory.getLogger(DeadLockDemo.class);

private static final Object resource1 = new Object();
private static final Object resource2 = new Object();

public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
logger.info("{} has resource 1", Thread.currentThread().getName());

try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("{} waiting for resource 2", Thread.currentThread().getName());
synchronized (resource2) {
logger.info("{} has resource 2", Thread.currentThread().getName());
}
}
}, "Thread A").start();

new Thread(() -> {
synchronized (resource2) {
logger.info("{} has resource 2", Thread.currentThread().getName());

try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("{} waiting for resource 1", Thread.currentThread().getName());
synchronized (resource1) {
logger.info("{} has resource 1", Thread.currentThread().getName());
}
}
}, "Thread B").start();
}
}

输出结果:

14:49:12.184 [Thread B] INFO com.juzi.juc.demo.DeadLockDemo - Thread B has resource 2
14:49:12.184 [Thread A] INFO com.juzi.juc.demo.DeadLockDemo - Thread A has resource 1
14:49:13.191 [Thread B] INFO com.juzi.juc.demo.DeadLockDemo - Thread B waiting for resource 1
14:49:13.191 [Thread A] INFO com.juzi.juc.demo.DeadLockDemo - Thread A waiting for resource 2

上述代码中,线程A通过synchorized(resource1)获得了资源1resource1的监视器锁,然后线程A休眠1s,这么做的目的是让线程B获取资源2resource2的监视器锁。当两个线程休眠结束之后,都开始请求获取对象已经占有的资源,然后这两个线程就会陷入互相等待的状态,也就产生了死锁。

上述的例子符合死锁产生的四个必要条件

  1. 互斥:资源任意一个时刻只由一个线程持有
  2. 请求和保持:一个线程因请求资源而阻塞时,对已获得的资源保持不释放
  3. 不可剥夺:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕之后才能释放资源
  4. 循环等待:若干线程之间形成头尾相接的循环等待资源关系

预防死锁

破坏死锁产生的必要条件即可。

  1. 破坏请求和保持条件:一次性申请所有的资源
  2. 破坏不可剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源
  3. 破坏循环等待条件:按顺序申请资源

避免死锁

在资源分配时,借助算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。

安全状态:系统能够按照某种线程推进顺序来为每个线程分配所需的资源,知道满足各个线程对资源的最大需求,使每个线程都可以顺利完成,那么这个顺序的序列就是安全序列。

sleep()wait()对比

同:两个方法都可以暂停线程的执行

异:

  1. 锁释放角度:sleep()方法没有释放占有的锁资源,wait()方法会释放锁
  2. 应用场景:sleep()方法通常被用于暂停执行,wait()方法通常用于线程间交互、通信
  3. 是否苏醒角度:调用wait()方法后,线程不会自动苏醒,需要其他线程调用同一对象上的notify()或者notifyAll()方法,调用wait(long timeout),超时后线程会自动苏醒;sleep()方法执行完成后,线程会自动苏醒
  4. 方法位置: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