内部属性
1 | //线程名,如果创建时没有指定则使用Thread- + 创建序列号 |
构造函数
1 | public Thread() { |
构造方法其实都是对Thread 内部属性进行初始化,比如线程名、优先级、类加器、线程Id。如果没有设置这些属性全部继承自当前的。让我比较奇怪是非常重要的threadStatus
没有赋值,而是使用了默认值,我猜想这个变量全程都是由c++来变更的,所以不必要使用Java进行赋值。
已经初始化的线程对象可以通过set方法去修改守护线程、线程名、优先级。
线程状态
1 | public enum State { |
线程状态经常被问于面试中,几个状态和代表涵义大家都有记一记。
状态 | 描述 | 场景 |
---|---|---|
NEW | Thread线程刚刚被创建,创建状态 | new Thread |
RUNNABLE | 运行状态,线程正在运行中 | Thread.start |
BLOCKED | 堵塞状态 | synchronized 竞争失败 |
WAITING | 等待,这种状态要么无限等待下去,要么被唤醒 | Object.wait、Lock |
TIMED_WAITING | 等待超时,在等待时设置了时间,到时会自动唤醒 | Thread.sleep、LockSupport.parkNanos |
TERMINATED | 死亡状态 | 线程已经执行完任务 |
从下图可以发现从创建-> 运行-> 死亡 这个过程是不可逆的。
线程运行和停止
1 | public synchronized void start() { |
start
方法比较简单的,先判断状态是否正确,在创建之前加入到线程组里面,失败了再移除。start0 方法应该就是调用系统资源真正去创建一个线程了,而且线程状态也是由这个方法修改的。
run
方法只有使用Thread来创建线程,并且使用Runnable传参才会执行这里run方法,继承方式应该是直接调用子类run方法了。
1 | public void run() { |
stop
方法虽然在Java2已经被官方停用了,很值得去了解下的。
1 | "1.2") (since= |
看完这个方法,也没有看出来stop()
能干什么,我也不是很清楚这个stop能干什么,我将写几个例子验证功能。
创建几个线程去执行下任务,执行一会后,对所有线程调用stop方法,是否会退出任务。
1 | public class ThreadStopTest { |
执行结果
1 | Thread-1 : 0 |
调用完stop方法,线程立刻退出任务,连一个异常都没有抛出的,真的是非常干脆。如果有人不下心使用stop方法,出现问题都非常难排除,所以Java 官方早早就停止使用它了,详细看官方说明
如果想优雅停止一个正在运行的线程,官方建议使用interrupted()
。线程中断就是目标线程发送一个中断信号,能够收到中断信号线程自己实现退出逻辑。简单点说就是线程A在干活,突然有个人对它做了一个动作,线程A在知道这个动作涵义,它会知道自己要停下来。说白这就一个动作,如果线程逻辑没有处理这个动作代码,线程并不会退出的。看下Thread类里面有那些方法。
方法 | 备注 |
---|---|
interrupt() | 中断目标线程,给目标线程发一个中断信号,线程被打上中断标记 |
isInterrupted() | 判断目标线程是否被中断,不会清除中断标记 |
interrupted | 判断目标线程是否被中断,会清除中断标记 |
实现一个简单例子
1 | public static void main(String[] args) throws InterruptedException { |
上面代码核心是中断状态,如果中断被清除了,那程序不会跳出while循环的,下面改一下,添加一个sleep方法
1 | public static void main(String[] args) throws InterruptedException { |
执行结果 : 发送中断后,Thread.sleep直接抛出一个异常,并不会跳出循环。
因为sleep会响应中断,抛出一个中断异常,再清除线程中断状态。再回到while 判断时,中断状态已经被清除了,继续循环下去。sleep()
是一个静态native 方法,使当前执行的线程休眠指定时间,但是休眠的线程不会放弃监控器的锁(synchronized),当任何线程要中断当前线程时,会抛出InterruptedException
异常,并且清理当前线程的中断状态。所以在方法调用上就会抛出这个异常,让调用者去处理中断异常。
join和yield方法
join()
就是一个等待方法,等待当前线程任务执行后,再次唤醒被调用的线程,常常用来控制多线程任务执行顺序。
1 | /** |
想了解方法主要看方法注释就行,在指定时间内等待被调用者的线程死亡,如果没有死亡时间到了会自行唤醒,如果时间为0则永远等待下去,直到执行线程执行完任务。唤醒是由notifyAll执行的,但是没看见在哪里执行这个方法。查了一下资料知道每个线程执行完成后都会调用exit()方法,在exit会调用notifyAll。yield()
: 单词翻译过来就是让步的意思。主要作用当线程获取到执行权时,调用这个方法会主动让出执行器,它跟上面wait、sleep 不同,线程状态是没有改变的,此时任然是RUN。比如一个线程获取锁失败了,这时线程什么不能干,获取锁本身是很快,此时将线程挂起了,有点得不偿失,不如此时让出CPU执行器,让其他线程去执行。既不会浪费CPU宝贵时间,也不需要太耗费性能。这个方法经常用于java.util.concurrent.locks
包下同步方法,看过并发工具类的同学应该都认识它。
线程间协作
wait方法让当前线程进入等待状态(WAITING),并且释放监控锁,只有当其他线程调用notify或者notifyAll才会唤醒线程。
notify唤醒一个在等待状态的线程,重新进入RUNNABLE状态。
notifyAll唤醒所有正在等待状态的线程,重新进入RUNNABLE状态。
上面三个方法都必须在监控锁(synchronized)下使用,不然会抛出IllegalMonitorStateException。
wait、notify 两个方法结合就可以实现线程之间协作。比如最经典的生产者-消费者模型: 当上游消费者发送发送信息太多,导致队列挤压已经满了,这时消费者这边可以使用wait,让生产者停下里,当消费者已经开始消费了,此时队列已经被消费走一个信息了,有空间了,消费者可以调用notify,让上游生产者继续运作起来。当队列里面信息已经被消费完时,消费者会调用wait,让线程进入等待中,当上游线程有信息发送到队列时,此时队列中信息就不是全空的了,就可以调用wait 唤醒一个等待消费者。这样就可以形成线程之间相互通信的效果了。
简单实现消费者-生产者模型
1 | public void push(T t){ |
Callable和 Thread关系
我们知道了所有的线程其实都是Thread.start去创建的,重写run 方法达到异常执行任务,但是Callable这个接口是否也是使用Thread或者Runnable接口,主要看FutureTask
就知道如何实现了。
看下run方法
1 | public void run() { |
可以看出Callable仍然是使用Thread来创建线程的,内部通过维护state来判断任务状态,在run 方法中执行call方法,保存异常和执行结果。
看下get() 如何获取执行结果的吧
1 | public V get() throws InterruptedException, ExecutionException { |
现在基本就明了,使用run 调用call方法,将执行结果保存起来,然后get 方法这边使用自旋方法等待执行结果,并且使用队列将等待的线程保存起来,来处理线程的唤醒、中断。
总结
这里简单说了Thread的构造方法,属性设置,比较重要就是线程几个状态,状态流转、线程启动停止,中断处理,几个常用方法的介绍。简单说了下FutureTask
实现原理,结合上面提到的知识点,上面提到这些知识都是挺重要的,你可以看到大部分Java并发类都用到这些知识来开发的,频繁出现在面试中也是可以理解的。