线程
线程的状态
- 创建 new
- 就绪 runnable
- 运行 running
- 阻塞 blocked
- time waiting 睡眠
- waiting 等待
- blocked 阻塞
- 死亡 dead
创建一个线程之后,不会立即进入就绪状态,因为运行线程需要一些条件(内存资源等)只有线程运行需要的所以条件满足之后才会进入就绪状态。
进入就绪状态之后,也并不代表就能立刻获取 CPU 执行时间,也许此时 CPU 正在执行其他事情,需要等待。得到 CPU 执行时间之后,才进入运行状态。
线程在运行状态过程中,可能有多个原因导致当前线程不继续运行下去,比如用户主动让线程睡眠(睡眠一定的时间之后)、用户主动让线程等待、被同步块阻塞,此时就对应多个状态。time waiting、waiting、blocked。
线程死亡的原因:
- run 方法执行完毕
- 线程抛出异常
- 调用 stop 方法
- 线程被中断
- JVM 退出
上下文切换
线程 上下文切换的过程中,会记录程序计数器、CPU 寄存器状态等数据。
线程的状态流转
线程的生命周期及五种基本状态:
Java线程具有五中基本状态
1)新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
2)就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
3)运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
4)阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
2.同步阻塞 — 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.其他阻塞 — 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时. join()等待线程终止或者超时. 或者I/O处理完毕时,线程重新转入就绪状态。
5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
什么是线程死锁?如何避免死锁?
死锁
- 多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
死锁必须具备以下四个条件:
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
如何避免线程死锁?
只要破坏产生死锁的四个条件中的其中一个就可以了
- 破坏互斥条件 这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)
- 破坏请求与保持条件 一次性申请所有的资源。
- 破坏不剥夺条件 占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件 靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
- 锁排序法:(必须回答出来的点) 指定获取锁的顺序,比如某个线程只有获得A锁和B锁,才能对某资源进行操作,在多线程条件下,如何避免死锁? 通过指定锁的获取顺序,比如规定,只有获得A锁的线程才有资格获取B锁,按顺序获取锁就可以避免死锁。这通常被认为是解决死锁很好的一种方法。
- 使用显式锁中的ReentrantLock.try(long,TimeUnit)来申请锁
常见的对比
Runnable VS Callable
- Callable仅在 Java 1.5 中引入,目的就是为了来处理Runnable不支持的用例。Callable 接口可以返回结果或抛出检查异常
- Runnable 接口不会返回结果或抛出检查异常,
- 如果任务不需要返回结果或抛出异常推荐使用 Runnable接口,这样代码看起来会更加简洁
- 工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。(Executors.callable(Runnable task)或 Executors.callable(Runnable task,Object resule))
shutdown() VS shutdownNow()
- shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。
- shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。 shutdownNow的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终
isTerminated() VS isShutdown()
- isShutDown 当调用 shutdown() 方法后返回为 true。
- isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true
sleep() 方法和 wait() 方法区别和共同点?
区别
- sleep方法:是Thread类的静态方法,当前线程将睡眠n毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进入可运行状态,等待CPU的到来。睡眠不释放锁(如果有的话)。
- wait方法:是Object的方法,必须与synchronized关键字一起使用,线程进入阻塞状态,当notify或者notifyall被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠时,会释放互斥锁。
- sleep 方法没有释放锁,而 wait 方法释放了锁 。
- sleep 通常被用于暂停执行Wait 通常被用于线程间交互/通信
- sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法
相同
- 两者都可以暂停线程的执行。
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法
- new 一个 Thread,线程进入了新建状态; 调用start() 会执行线程的相 应准备工作,然后自动执行 run() 方法的内容,(调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。)这是真正的多线程工作。
- 直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
JVM执行start方法,会另起一条线程执行thread的run方法,这才起到多线程的效果~
如果直接调用Thread的run()方法,其方法还是运行在主线程中,没有起到多线程效果。
Thread类中的 yield 方法有什么作用?
Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。 它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU, 执行yield()的线程有可能在进入到暂停状态后马上又被执行。
volatile 关键字
volatile的两层语义:
-
volatile保证变量对所有线程的可见性:当volatile变量被修改,新值对所有线程会立即更新。或者理解为多线程环境下使用volatile修饰的变量的值一定是最新的。
-
jdk1.5以后volatile完全避免了指令重排优化,实现了有序性。
线程阻塞的三种情况
当线程因为某种原因放弃 CPU 使用权后,即让出了 CPU 时间片,暂时就会停止运行,知道线程进入可运行状态(Runnable
),才有机会再次获得 CPU 时间片转入 RUNNING
状态。一般来讲,阻塞的情况可以分为如下三种:
- 等待阻塞(Object.wait -> 等待队列)
RUNNING
状态的线程执行 Object.wait()
方法后,JVM 会将线程放入等待序列(waitting queue);
- 同步阻塞(lock -> 锁池)
RUNNING
状态的线程在获取对象的同步锁时,若该 同步锁被其他线程占用,则 JVM 将该线程放入锁池(lock pool)中;
- 其他阻塞(sleep/join)
RUNNING
状态的线程执行 Thread.sleep(long ms)
或 Thread.join()
方法,或发出 I/O 请求时,JVM 会将该线程置为阻塞状态。当 sleep()
状态超时,join()
等待线程终止或超时. 或者 I/O 处理完毕时,线程重新转入可运行状态(RUNNABLE
);
线程死亡的三种方式
- 正常结束
run()
或者 call()
方法执行完成后,线程正常结束;
- 异常结束
线程抛出一个未捕获的 Exception
或 Error
,导致线程异常结束;
- 调用 stop()
直接调用线程的 stop()
方法来结束该线程,但是一般不推荐使用该种方式,因为该方法通常容易导致死锁;
守护线程是什么?
守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。