阅读 601

线程总结

什么是线程

维基百科中给到的解释

线程:是操作系统能够进行运算调度的最小单位,大部分情况下,它被包含在进程之中,是进程中的实际运作单位

线程区别于协程。线程是抢占式的,在单CPU单核的计算机上。一次性只能有一个线程处理任务,所谓的多线程,是多个线程相互抢占CPU处理自己的任务。

那为什么多线程能够提高运算能力? 在维基百科这么形容

因为使用多线程技术,也可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,从而提高了程序的执行效率

笔者理解,计算机处理一个任务。并不是一直不停的执行,而是在不停的调度,例如,在线程A空闲的时候,去执行线程B的任务,从而提高效率。

线程,进程,协程的关系

如上面解释什么是线程。线程是包含在进程之中。而协程更像是线程中的线程,协程相对于线程而言,更加轻量级。如果非要说他们之间的关系。笔者认为,就是没有关系。

协程本身就是一个很大的知识点。之后如果有时间,笔者会对协程进行知识总结。目前在看一本Kotlin协程这一本书。协程使用很简单,而理解它还是有点难的。笔者对协程的了解不是太清楚。还停留在使用阶段。。

如何启动线程

启动线程的方式有两种。

  • 继承Thread
  • 实现Runnable接口

实际使用中很少会继承Thread,只要有下面几个原因

  • JAVA是单继承。如果使用继承Thread的方式。就很大程度上限制了开发者去复用代码块。
  • Runnable接口可以多处复用。在JAVA中。接口没有单继承的限制,方便拓展和代码的复用

🌰举例说明

使用继承的方式

实现Runnable接口的方式,更容易做到拓展

Thread类中的start()和run()方法有什么区别?

先来一个面试题,请问下面代码。执行的是在哪个线程?

恭喜回答主线程的同学,回答对了,没错。运行结果如下。是在主线程。

其实比较好理解。run()方法相当于你直接执行了代码。原本在哪个线程,还是在那个线程。等同于写了一个普通方法。

start()则是告诉计算机。线程已经准备好了,随时等着CPU调度,等CPU调度选中了这个线程。那么才会执行run()方法,改成start()再次运行。结果很明显

线程有几种状态?

Thread源码中又分成了6种。 维基百科中又定义成了4种

其实这么回答都没错,只是选择的角度不同。所以认定的状态也不相同。

一般理解的5种状态

笔者认为下面的5种状态的描述,更容易能够认知到线程。

图片来源 线程5种状态及常见问题

创建状态

执行了new Thread(),创建线程,并且为其分配相应的内存空间和资源。此时并没有开始执行

就绪状态

执行了start()方法。进入线程队列排队。等待CPU调度

运行状态

被CPU调度选中,获取处理器资源。执行run方法的代码

阻塞状态

遇到人为挂起或者需要执行耗时的操作,让出CPU资源暂停执行。

  • 阻塞时,不在进入线程队列排队
  • 阻塞消除以后,进入就绪状态

终止状态

即死亡状态,run()任务执行完成,或者执行stop()destroy(),线程结束。

源码中的6种状态

在Java线程的源码中分成了6种状态.先看一张比较经典的图

再来看一下源码中对状态的枚举定义。

public enum State {
   //新创建
    NEW,
    //可运行
    RUNNABLE,
    //被阻塞
    BLOCKED,
    //等待
    WAITING,
    //计时等待
    TIMED_WAITING,
    //被终止
    TERMINATED;
}
复制代码

不难发现。大体上和上面笔者认为比较好理解的5种状态差不多。只是多了等待计时等待。下来一起分析一下每个状态的含义。以及实例。

NEW

其中NEW可以理解成new Thread(),分配了内存和资源对应上面理解的创建状态,比较好理解

RUNNABLE

RUNNABLE状态可理解成上面的就绪状态运行状态。执行了start()方法。已经处于线程队列排队,并且可能已经被CPU调度选中,进入运行状态,在JVM层面统称为RUNNABLE可运行状态。

TERMINATED

对应的是上面的终止状态

🌰举例说明

下面代码就很好的演示了,线程t1的状态,刚开始的创建new Thread()状态是NEW,等待1S以后执行start(),t1的状态变成了RUNNABLE,执行完成以后等待1S,在看一下t1的状态就变成了TERMINATED

最终执行的结果为

BLOCKED

阻塞状态BLOCKED,被synchronized块阻塞,例如在多线程中。线程A获取锁进入同步块,在其出来之前,如果线程B想进入,就会因为获取不到锁而阻塞在同步块之外,这时线程B的状态就是BLOCKED

🌰举例说明

下面代码有两个线程t1t2,线程t1先执行。执行任务5S,并且持有block对象的,等待1S,确保线程t1顺利执行。1S后,线程t2准备执行任务,却发现没有,此时的锁还在线程t1手上。并没释放。那么线程t2的状态就是BLOCKED

执行结果为

WAITING

所谓的等待,是未满足特定条件下,线程执行了wait()操作,例如线程A需要满足存款大于200元才执行其他操作,如果未能满足存款大于200元的特定条件。执行了wait()操作,此时线程A进入WAITING等待状态,等待其他线程发出notify或者notifyAll的通知。线程A收到通知以后,会重新进入RUNNABLE状态。

🌰举例说明

下面代码线程t1先执行。发现存款少于200,执行了wait(),此时线程t的状态就是WAITING。线程t2给设置到了300元。并且触发了notify(),线程t1被重新调度以后,顺利执行。

TIMED_WAITING

WAITING相似。区别在于使用wait(time),即超时时间多久。wait()则是无限等待。

运行结果

join()有什么作用

join方法的注释上这么描述

Waits for this thread to die.

翻译过来就是等待线程死亡。意思是。只有等这个线程结束以后才会执行下面的代码。

🌰举例说明

未添加join运行结果

未添加join()方法,程序都没有将t1 finish打印出来。因为对于main线程来说。他已经执行结束了。也不去关心t1线程是否执行完了。

添加join 运行结果

添加join()方法。main线程只能等着。因为t1线程还没执行完成。只有等t1线程执行完了。main线程才能继续执行。

sleep(),wait()的区别

  • sleep()是线程的方法。而wait()是Object的方法
  • sleep()和wait()都会释放cpu资源,而sleep()不会释放锁,wait()会释放锁

针对第二个差别,笔者这里可准备一个小的测试代码,请问下面代码 t1t2的状态是什么。

先看一下。笔者这里准备了两个线程,t1线程先执行,并且获取了,然后执行4S,这个时候会释放CPU资源。那么线程t2准备执行。但是因为现在的还在线程t1手上。所以。线程t1的状态应该是TIMED_WAITING等待状态。而线程t2因为获取不到锁而阻塞在同步块之外,其状态是BLOCKED运行结果如下,你答对了嘛

如果此时将Thread.sleep(time);代码替换成obj.wait(time),请问 t1t2的状态是什么。

读者可以想一下。本小节的区别sleep()不会释放锁,wait()会释放锁,所以在t1wait的时候,t2开始执行。然后t1t2都等待着被唤醒。可惜啊。没人唤醒他们。运行结果如下。读者答对没有呢

yield()作用

源码中注释这么标注

A hint to the scheduler that the current thread is willing to yield

表示当前线程愿意让步。其本意是说,放弃cpu的资源。主动回归到等待队列中。让cpu进行调度。但是回归了队列,意味着依然有可能被重新选中。

🌰举例说明

准备了两个线程。t0t1,让他们打印0到4,并且在打印到第2个以后,yield()一下。按照我们的理解,他应该有两种可能。

  • 回归等待队列以后。又被重新选中了。prepare yield之后。继续执行当前线程的任务
  • 回归等待队列以后。没有重新,让步给了其他线程。prepare yield之后。执行其他线程的任务

死锁是什么

百度百科中解释

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程

🌰举例说明

注意,之前的测试都是在测试代码中运行。这里是运行在main方法中,部分新的gralde版本在运行main方法会抛出错误。main not found,只需要在项目目录下的.idea/gradle.xml中添加即可。

<option name="delegatedBuild" value="false" />
复制代码

下面代码运行就发生死锁。线程t1锁住了obj1,线程t2锁住了obj2,就导致了死锁。

运行结果如下。注意看红框表示线程还在运行。并没有结束。

如何终止线程

正常退出

比较好理解。run()内的方法全部执行完成。线程正常结束。并且推出。就像在上面举例说明 NEW->RUNNABLE->TERMINATED 的过程。就是正常的退出

stop/destroy

Thread提供了 stopdestroy的方法用于退出程序。本质上他们都是抛出异常终止线程,可以看一下他们的源码。这种属于强行使用异常中断,并且方法被标记为废弃Deprecated.不建议使用

interrupt

本质上是通过标记。判断状态。如果遇到线程正在等待的状态TIMED_WAITING。会抛出java.lang.InterruptedException: sleep interrupted的异常。

运行结果

如果是正常运行状态,则可以通过通过interrupted()判断

参考

文章分类
Android
文章标签