此次为了自己复习,所以简单总结了下面试常见的一些线程基础知识点,如果有需要修正的地方欢迎大家随时指出
1、线程的几种状态
NEW 新建状态
- 当一个线程创建之后,如果还没有调用start()方法,此时就是NEW,新建状态
Runnable 可运行状态
- 线程一旦调用start()方法,就会进入Runnable,Runnable又分为ready与running,ready也就是就绪状态,等待分配CPU资源,一旦分配到CPU资源,开始执行run()方法,就会进入running运行中状态
阻塞状态 (Blocked、Waiting、Timed Waiting统称为阻塞状态)
- Blocked(被阻塞)
- 从Runnable到Blocked只有一种可能,那就是进入了synchronized 保护的代码时没有抢到 monitor 锁,当拿到 monitor 锁时又会进入Runnable
- Waiting(等待)
- 进入Waiting有几种情况,通过调用Object.wait()、Thread.join(),LockSupport.park()等方法都会进入该状态,Blocked和Waiting的不同是Blocked在等待别的线程释放monitor锁,而Waiting是在等待某个条件,比如notify()/notifyAll(),或者join的线程执行完毕
- Timed Waiting(计时等待)
- Timed Waiting和Waiting类似,只不过有时间限制,超时之后会由系统自动唤醒,Thread.sleep(long millis)、Object.wait(long timeout)、Thread.join(long millis)、LockSupport.parkNanos(long nanos) 等方法都会使线程进入该状态
Terminated(被终止)
- run方法执行完毕正常退出、出现未捕获的异常都会使线程终止,我们也可以通过调用一些方法手动让线程终止,例如stop()、interrupt()方法,stop()方法会立刻停止当前线程,不会执行当前线程执行剩下的逻辑,可能会出现业务的完整性问题,现在已经不推荐使用;interrupt()方法不会终止线程,而是给线程打一个中断标记,可以通过调用interrupted()方法判断这个标记,来决定是否结束该线程
注意:
1、Blocked与Timed Waiting这俩种状态都会主动恢复为Runnable状态,但是Waiting状态比较特殊,需要搭配一些别的方法才会恢复,例如Object.wait()搭配Object.notify()/notifyAll()、LockSupport.park()搭配LockSupport.unpark()
2、线程的状态流转是按照以上描述的状态顺序所走的,NEW不可直接进入Blocked,需要通过Runnable才可以,并且线程的状态是不可逆转的,Runnable不可回退成NEW,Terminated也不可回退成Runnable
2、创建线程的方式
大家经常说创建线程的方式是实现Runnable接口和继承Thread类,或者是使用线程池与匿名类等等,其实这并不是创建线程的方式,创建线程的方式本质上其实只有一种,就是new Thread(),而Runnable接口和继承Thread类是实现线程执行内容的方式,并不是创建线程的方式;对于线程池而言,它使用线程工厂来创建的,而它底层最终还是调用的new Thread(),所以说线程池本质上也是使用的new Thread()来创建线程
3、实现Runnable接口和继承Thread类的区别
- 底层实现
- 实现Runnable接口的方式,在创建线程时将该类传进去,底层在run方法中调用了该类,以下为源码实现
// Thread属性
private Runnable target;
// Thread构造方法
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
// Thread的run方法
public void run() {
if (target != null) {
target.run();
}
}- 继承Thread类的方式其实就是重写了Thread类的run()方法
- Java是单继承,继承了Thread类就无法继承别的类,而实现Runnable接口的方式就没有该限制
- 实现Runnable接口的方式可以看作将业务实现与Thread本身解偶,不干扰Thread本身
- 在某些情况下,实现Runnable接口可以提高性能;使用继承Thread类的方式,在线程执行完被销毁之后,如果还想执行任务,就需要重新创建;而使用实现Runnable接口的方式,可将任务直接传进线程池,达到线程的复用
4、线程的一些常用方法
- join():保证线程之间可以串行执行,会阻塞主线程,等待当前调用的线程执行完run()方法后再执行join之后的代码
- yield():Thread方法,让出cpu资源给同优先级的线程获得执行机会,如果没有同优先级线程,则会继续执行当前线程
- sleep(long millis):Thread类方法,必须指定休眠时间,使当前线程阻塞,在同步代码中执行时不会释放monitor锁,超时之后会主动恢复
- wait():Object类方法,使当前线程阻塞,会释放monitor锁,并进入等待队列,等别的线程调用notify或者notifyAll时,线程会从等待队列进入到同步队列中
- notify():随机唤醒一个等待的线程进入同步队列中
- notifyAll():唤醒所有线程进入同步队列中
注:wait() 方法必须在 synchronized 保护的同步代码中使用,并且wait()方法需要与notify()/notifyAll()搭配使用
5、多线程带来的性能问题
- 上下文切换:CPU为了保证每个线程都有机会执行,在调度时就会引起上下文切换,上下文切换会挂起当前线程,唤醒别的线程,把资源分配给别的线程执行,上下文切换带来的开销是比较大的,如果我们要执行的代码非常快,那有可能上下文切换的开销比线程执行业务代码的性能开销还大
- 缓存失效:CPU为了加快程序的性能,会将一些数据从内存中放置缓存中,从缓存中读取数据的速度要比从内存中读要很快,如果进行了线程调度,切换到别的线程,那原有的缓存有可能失效了,那么就需要缓存新的数据,这也会带来一定的性能开销
- 协助开销:在使用多线程的时候,为了线程安全,经常会禁用编译器和CPU的重排序,有时为了数据的准确性,会不断的将工作内存中的数据刷到主内存中,主内存中的数据刷到工作内存中,这些也间接降低了性能