并发编程-锁的那些事儿【六:Java线程的生命周期】

前言

今天这篇我们需要来了解下Java的线程, 在前几篇里面我们总是会提到这么几个词: “用户态”,“内核态”。。。 这些到底啥,而且为啥跟加锁以及性能会扯上关系呢? 其实很简单,对于并发编程来说,说白就是线程之间的通信,如果能够让线程间做的“分工”,“协作”,“互斥”,那么一定意义上就可以实现并发。 但线程不是人呀,人是一种高等生物,咱们的大脑可以做出随意应变的思考,但想要线程协同做事,那么就得基于硬件的基础,给系统带来各个不同阶段的状态,通过不同时段需要进行的操作,去调整线程状态,从而达到线程之间相互交流。 这个线程状态跟人一样,都是有一个生老病死的过程,我们称之为生命周期;

Java线程状态分析

在Java语言中,一共有六中不同的状态,分别如下:

  1. NEW(初始化状态)

  2. RUNNABLE(可运行 / 运行状态)

  3. BLOCKED(阻塞状态)

  4. WAITING(无时限等待)

  5. TIMED_WAITING(有时限等待)

  6. TERMINATED(终止状态)

嗯,看这挺多的。 通过字面大可分下类: 初始化阶段----->运行阶段------>阻塞阶段--------->终止阶段; 其中“阻塞阶段” 与 “运行阶段” 是一个颠来倒去跨阶段转换,看下面,我们逐一分析;

初始化阶段

这一阶段对应上诉“NEW(初始化状态)” ,它存在意义就在于:咱们要想使用这个应用,就得在给人家创建可以运行的环境, 也就是创造一个线程出来, 注意这个阶段,还没有分配CPU资源;

运行阶段

对应上述“RUNNABLE(可运行 / 运行状态)”,这阶段简单理解, 如果在单核环境下,那逐一创建线程在去获取cpu资源,然后就具备了线程执行的能力,他们就可以实际去执行指令代码。如在多核环境下,又同时创造了多条线程,那么多个线程就开始抢夺CPU的资源,前面那么多篇说的有关锁资源抢占,就是从这个阶段已经开始了。

阻塞阶段

对应上述 “BLOCKED(阻塞状态)”,“WAITING(无时限等待)”,“TIMED_WAITING(有时限等待)”,这个能够进入这个阶段,那么前面的线程状态必然已经到达运行状态,如果此时code中有一个阻塞API(例如阻塞读取文件,或者等待资源释放中),那么就会就会进入这个阶段,这个是三状态,共同之处,就是会放弃对CPU的占用,线程状态从运行转换为阻塞; 在平常的并发开发中往往都是运行与阻塞俩个状态在互相转换。

终止阶段

对应上述“TERMINATED(终止状态)”,当线程执行完成,或者遇到异常就行进入终止阶段,一旦进入此状态,一位线程完全死掉,不会在进行状态转换了。

经过上面这些概念了解后,主要是我们要来分析,运行阶段与阻塞阶段中的不同状态的转换。 这些转换原理,弄得门清后,对于后面并发深入有着事半功倍的效果,我们接着往下盘;

##线程状态之间的转换

初始化阶段----->运行阶段------>阻塞阶段--------->终止阶段 这个阶段中的状态,是不可以互相转换,只能阶段之间的状态转,从首阶段依次往后分析:

初始化阶段----->运行阶段[NEW 到 RUNNABLE 状态]

这个状态的转换是最简单的。 NEW是指初始化线程,在代码实现有三种形式,分别如下:
1、继承 Thread 对象,重写 run() 方法

/**
 * 继承Thread,重写run
 */
public class ThreadTest extends Thread {
    @Override
    public void run(){
        //业务实现
    }
}

2、实现runnable结构,重写run

/**
 * 实现runnable结构,重写run
 */
class RunnableTest implements Runnable{

    @Override
    public void run() {
        //业务实现
    }
}

3、实现Callable结构,重写run

/**
 * 实现Callable结构,重写run
 */
class CallableTest implements Callable {

    @Override
    public Object call() throws Exception {
        //业务实现
        return null;
    }
}
 
  

通过上述这三方式,既可创建一个Java线程。但NEW状态,不会被系统调度因此不会执行的。当code执行到此处,就会转成RUNNABLE状态;

class ThreadRunTest{

    public static void main(String[] args) throws Exception {

        //通过下面这三种方式,启动线程,既可以从NEW转为RUNNABLE
        ThreadTest threadTest = new ThreadTest();
        threadTest.run();

        Thread runableTest = new Thread(new RunnableTest());
        runableTest.run();

        Callable callableTest = new CallableTest();
        callableTest.call();
    }
}
 
  

运行阶段----->阻塞阶段

1、RUNNABLE 与 BLOCKED状态转换
场景: 使用synchronize隐式锁,修饰的代码块或者方法。当被加锁后,代码块同一时刻只能被某一个线程占用,其他线程等待。 那么等待的线程则为BLOCKED;

    public void synchronizeTest(){
        
        synchronized (this){
            System.out.println("集体业务实现");
        }
        
    }

2、RUNNABLE 与 WAITING状态转换
场景:

  • Object.wait()—一定基于在synchronize隐式锁修饰的代码块中调用,不然会抛异常,显示无监听器的异常。 当前线程释放对象锁,进入等待队列

  • Thread.join()—线程同步方法,计划B线程在A线程完后在执行,那么在A线程还没结束时,B线程将会一直阻塞,不释放对象锁,等待A线程完成唤醒,当前线程进入可运行状态

  • LockSupport.park()—此方法与Thread.join差不多一致。 不同之处,体现在用法上;
    代码使用以上三个方法,那么就会转为WAITING

3、RUNNABLE 与 TIMED_WAITING状态转换
场景:

  • 获得 synchronized 隐式锁的线程,调用带超时参数的 Object.wait(long timeout) 方法;

  • Thread.join(long millis) —不释放对象锁,直到millis时间到,当前线程进入可运行状态

  • Thread.sleep(long millis)—不释放对象锁,millis后线程自动苏醒进入可运行状态

  • LockSupport.parkNanos(Object blocker, long deadline)

  • LockSupport.parkUntil(long deadline)

以上5中方式,与WAITING转换最大区别在于,这个是在有效时间后,会自动唤醒线程,转为RUNNABLE。 而WAITING更加偏向 无限时间的等待,必须需要另一个线程通知唤醒;

4、RUNNABLE 到 TERMINATED 状态
场景:

  • 线程的run方法,正确执行完毕;

  • Thread.stop()----此方法不建议使用,因为如果当线程还有synchronize锁加持时,stop方法是直接kill线程,那么还没有释放synchronize锁,就不会被释放了。

  • Thread.interrupt()----这个是线程中断方法。一种优雅停止线程的方式。 当线程出于阻塞阶段,其他线程调用阻塞线程的Thread.interrupt,那么就会阻塞方法状态转为RUNNABLE,在抛出InterruptedException异常,中断线程。 so在上述阻塞场景中提到的线程方法,都throws InterruptedException 这个异常。

最后用一幅图来更加清楚的理解下:
并发编程-锁的那些事儿【六:Java线程的生命周期】_第1张图片

你可能感兴趣的:(并发编程)