什么是线程
维基百科中给到的解释
线程:是操作系统能够进行运算调度的最小单位,大部分情况下,它被包含在
进程之中,是进程中的实际运作单位
线程区别于协程。线程是抢占式的,在单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种状态的描述,更容易能够认知到线程。
创建状态
执行了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
🌰举例说明
下面代码有两个线程t1和t2,线程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()会释放锁
针对第二个差别,笔者这里可准备一个小的测试代码,请问下面代码 t1和t2的状态是什么。
先看一下。笔者这里准备了两个线程,t1线程先执行,并且获取了锁,然后执行4S,这个时候会释放CPU资源。那么线程t2准备执行。但是因为现在的锁还在线程t1手上。所以。线程t1的状态应该是TIMED_WAITING等待状态。而线程t2因为获取不到锁而阻塞在同步块之外,其状态是BLOCKED运行结果如下,你答对了嘛
如果此时将Thread.sleep(time);代码替换成obj.wait(time),请问 t1和t2的状态是什么。
读者可以想一下。本小节的区别sleep()不会释放锁,wait()会释放锁,所以在t1wait的时候,t2开始执行。然后t1和t2都等待着被唤醒。可惜啊。没人唤醒他们。运行结果如下。读者答对没有呢
yield()作用
源码中注释这么标注
A hint to the scheduler that the current thread is willing to yield
表示当前线程愿意让步。其本意是说,放弃cpu的资源。主动回归到等待队列中。让cpu进行调度。但是回归了队列,意味着依然有可能被重新选中。
🌰举例说明
准备了两个线程。t0和t1,让他们打印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提供了 stop和destroy的方法用于退出程序。本质上他们都是抛出异常终止线程,可以看一下他们的源码。这种属于强行使用异常中断,并且方法被标记为废弃Deprecated.不建议使用
interrupt
本质上是通过标记。判断状态。如果遇到线程正在等待的状态TIMED_WAITING。会抛出java.lang.InterruptedException: sleep interrupted的异常。
运行结果
如果是正常运行状态,则可以通过通过interrupted()判断