【Java多线程学习2】线程的生命周期,线程上下文切换,什么是死锁及如何预防死锁等

一、线程的生命周期和状态

  • 1、New:创建状态、初始状态。线程被创建出来但是没有被调用 start() 。
  • 2、Runnable:就绪状态。当新建的线程被启动后(新建的线程被调用了start()方法),就进如了就绪状态,等待系统分配CPU资源。
  • 3、Running:运行状态。就绪的线程获得了CPU时间片(timeslice)后就处于Running运行状态。
  • 4、Blocked:阻塞状态。线程在等待获取锁等待输入/输出完成等情况下会进入阻塞状态。阻塞状态下,线程暂时停止执行,当获取到锁或满足条件后,线程会再次进入就绪状态
  • 5、Waiting:等待状态。线程在调用Object.wait()方法、Thread.join()方法时,线程会进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到就绪状态。
  • 6、Timed Waiting:超时等待状态。可以在指定时间后自行返回而不是像Waiting那样一直等待。
  • 7、Terminated:终止状态。表示该线程已经运行完毕。当线程执行完毕或是出现异常时,线程进入终止状态。处于终止状态的线程其资源会被释放。

【Java多线程学习2】线程的生命周期,线程上下文切换,什么是死锁及如何预防死锁等_第1张图片

二、什么是线程上下文切换

问题一:什么是上下文切换,上下文是指什么?上下文切换发生的场景

线程上下文切换是指:在多线程环境下,CPU从一个线程切换到另一个线程时,保存当前线程的上下文信息,并加载另一个线程上下文信息的过程。

上下文信息指:线程在执行过程中自己的运行条件和状态,比如,程序计数器栈信息寄存器的值等(寄存器是指CPU内部一组高速存储单元,用于临时存储和操作数据)

线程上下文切换通常发生在以下几种情况:

  • 线程主动让出CPU,比如调用了Thread.sleep(),Object.wait() 等。
  • 一个线程的时间片用完,需要切换到另一个线程继续执行。因为操作系统要防止一个线程或进程长时间占用CPU导致其他线程或者进程饿死。
  • 当线程因为阻塞或者等待某个事件而无法继续执行,调度器会切换到另一个线程继续执行。

线程上下文切换的开销较大,涉及到保存当前线程的上下文信息和加载下一个线程的上下文信息。频繁的上下文切换会降低系统的性能。

问题二:如何解决上下文切换?

1、无锁编程:采用无所编程或是CAS操作可以避免线程阻塞,从而减少线程上下文切换的次数。
2、减少线程数量:适当减少线程数量,进而减少上下文切换次数的发生。通过使用线程池技术控制线程数量的上限并合理控制线程的执行。

三、什么是死锁?如何避免死锁?

问题一:什么是死锁?

死锁是指两个或多个线程(或进程)在互斥的请求资源时因竞争而造成一种相互等待的状态,导致程序无法执行下去。
在死锁状态下,每个线程都在等待其他线程释放所占用的资源,而无法继续执行。

举个例子:
如下图所示,线程A持有资源2,在等待请求资源1;同时,线程B持有资源1,在等待请求资源2。他们同时都想申请到对方的资源,所以线程A和B就会相互等待而进入死锁状态。
【Java多线程学习2】线程的生命周期,线程上下文切换,什么是死锁及如何预防死锁等_第2张图片
下面举个例子说明上述死锁情况:

public class practice2 {
    private static Object resource1 = new Object();
    private static Object resource2 = new Object();

    public static void main(String[] args) {
        //线程A持有锁resource1,但他在尝试请求对象锁resource2
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //throw new RuntimeException(e);
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程A").start();

        //线程B持有锁resource2,但它在尝试请求对象锁resource1
        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //throw new RuntimeException(e);
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }

        }, "线程B").start();
    }
}

输出结果:
【Java多线程学习2】线程的生命周期,线程上下文切换,什么是死锁及如何预防死锁等_第3张图片
线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。

产生死锁的四个必要条件:

  • 1、互斥条件:每个资源在同一个时刻,只能被一个线程(进程)占有。
  • 2、请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  • 3、不可剥夺条件:线程已获取的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完后才能释放资源。
  • 4、循环等待条件:若干线程之间形成一种头尾相接的循环等待资源的关系。

问题二:如何预防和避免死锁?

如何预防死锁?
破坏死锁产生的必要条件即可:
1、破坏请求与保持条件:一次性的申请所有的资源。
2、破坏不可剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到可以主动释放它所占有的资源。
3、破坏循环等待条件:按照某一顺序申请资源,释放资源则反序释放。破坏等待条件。

如何避免死锁?
使用资源的有序性:避免死锁就是在资源分配时,按照约定的顺序来使用资源,以避免不同线程由于竞争同一资源而产生死锁。

四、sleep() 方法 和 wait() 方法对比

共同点
Thread.sleep() 和 Object.wait() 方法都可以暂停线程的执行。

区别:

  • 调用sleep() 方法时线程不会释放已持有的锁;而调用wait() 方法,线程会释放它所持有的锁
  • 调用sleep() 方法只是暂停一段指定时常的时间,之后线程会自动苏醒;而调用wait() 方法线程不会自动苏醒,需要别的线程调用同一对象的notify() 或 notifyAll() 方法唤醒它,或者也可以使用wait(long timeout) 超时后线程自动苏醒。
  • sleep()Thread类的静态本地方法wait() 则是 Object类的本地方法

举个例子:

public class practice2 {
    private static Object resource1 = new Object();

    public static void main(String[] args) {
        //
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println("线程1开始执行。。。。");
                try {
                    //线程1暂停执行,并释放对象锁
                    resource1.wait();
                } catch (InterruptedException e) {
                    //throw new RuntimeException(e);
                    e.printStackTrace();
                }
                System.out.println("线程1继续执行。。。。");
            }
        }, "线程1").start();


        new Thread(() -> {
            synchronized (resource1) {
                System.out.println("线程2开始执行。。。。");
                resource1.notify();

                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程2执行完成。。。。");
            }
        }, "线程2").start();
    }
}

输出结果:
【Java多线程学习2】线程的生命周期,线程上下文切换,什么是死锁及如何预防死锁等_第4张图片
注意:notify() 方法只会唤醒等待的线程,并不会释放对象锁,实际上notify() 方法执行完成后,仍然会继续执行notify() 所在代码块的声音代码,直到剩余代码执行完或再次调用了wait() 方法才会释放对象锁。
因此在调用wait() 或 notify()方法时,需要使用synchronized关键字保证线程安全。

你可能感兴趣的:(java,学习)