Java多线程基础
线程是一个轻量级的“进程”, 是程序最小的执行单位。了解多线程,我们从线程的生命周期、基本操作、线程安全等几个方面来入手。
生命周期
在Java中一个线程的生命周期状态有以下六种(都在Thread.State枚举中):
1 |
|
可以简单画一张图来描述Java线程的生命周期:
基本操作
其实在上面的线程状态图中已经包含了大部分线程的基本操作了:
new 一个线程对象,用Thread#start()启动线程。
Thread#start()
会让这个线程执行Thread#run()
,但Thread#run
默认的实现为空,故在new
一个线程对象时需要重写(@Override
)run()
方法,实现自己的任务逻辑。
tips: start()会调用native方法start0(),继而由 JVM 来实现多线程的控制,因为需要系统调用来控制时间分片
我们可以通过继承Thread类来重写run方法,实现自定义任务。
Java是单继承(只能继承一个类)多实现(可以实现多个接口),由于单继承的局限性,更推荐通过实现Runnable
接口的run()
方法来实现自定义任务(实际上该接口也只有这一个方法).
Thread类有一个重要的构造方法: public Thread(Runnable target)
,看到target我们自然而然就回想到代理模式。实际上Thread和Runnable就是一种静态代理的实现,参考以下代码(JDK8部分源码)
1 |
|
线程终止与中断
线程终止的情况:
- 正常执行结束
- 调用Thread#stop()方法暴力终止。由于其会把执行一半的线程强行终止,可能还会引起数据不一致问题,目前已被废弃(@Deprecated)
stop已被废弃,JDK提供了一个更为强大的支持,线程中断:
1 |
|
线程中断并不会使线程立马终止,而是给线程发送一个通知,告知线程目标需要终止,线程具体在什么时候退出,由目标线程自行决定(通过isInterrupted判断状态,决定怎么退出以及退出逻辑)。
关于中断还有中断响应这个概念,所有抛出InterruptedException的方法都可以响应中断,例如:
1 |
|
tips: 对抛出的InterruptedException进行捕获会清除中断标记,如果后续还需要判断中断状态,需要再次设置中断标记Thread.currentThread.interrupt()
wait()和notify()。
wait() 和 notify()是Object类中的两个方法。用于对多线程协作进行支持。
obj.wait()并不是随便可以调用的,它 必须包含在对应synchronized(obj) 语句中 ,因为无论是wait()还是notify()都需要先获得一个目标对象的监视器 (源码中wait()的注释This method should only be called by a thread that is the owner of this object's monitor.)
。notify() 也一样。
obj.wait()使持有obj对象锁的线程进入等待,并释放资源 让其他等待obj对象的线程可以正常执行,obj.notify()会 随机 唤醒一个等待中的线程, 但不会立即释放资源 需要等待当前线程的synchronized语句执行完毕才会释放,还有一个obj.notifyAll()会唤醒所有等待的线程.
tips: 实际上, hotspot JVM 中notify()是顺序唤醒的。参考文章
tips: sleep()方法也会让线程等待若干时间,但并不释放锁资源,也不能被通知唤醒。 但wait() 和 sleep() 都可响应中断。sleep()会使线程进入TIMED_WAITING状态
tips: 使用jstack命令可以查看当前系统的线程信息。配合jps使用
tips: 推荐wait()和notify()配合使用来替换挂起suspend()和继续执行resume()这两个已被废弃的方法。线程挂起suspend()并不会释放资源,若使用不当,resume()方法在suspend()之前执行了,还会导致死锁。被挂起的线程状态还是Runnable
join() 和 yield()
join() 字面意思是加入,但其在Java中其实也是一种等待,其核心代码如下,调用了wait(0)
方法.但join()不用notify通知唤醒,它会在被阻塞线程执行完毕后自动释放资源。
适用场景: 线程A的输入依赖与线程B的输出,则就需要使用B.join(),等待B执行完毕,A再继续执行
1 |
|
tips: 由于join() 实际调用wait() 故在开发中不用在Thread对象上使用wait() , notify(),因为这样可能会和系统api相互影响产生预期之外的错误。
yield()是一个静态native方法,使用yield() 会使当前线程让出CPU,之后和其他线程再次竞争CPU资源。
使用场景:一个线程不怎么重要,或者优先级很低,怎可以在适当的时候调用yield() 让出资源,给其他线程更多机会。
其他操作
- 设置后台守护进程(Daemon) ,如垃圾回收线程,JIT线程
1 |
|
- 设置线程优先级(Priority), 优先级从1到10,数字越大优先级越高,高的优先级并不表示一定会最先执行.Thread类默认给了三个优先级 1, 5, 10.
1 |
|
线程安全概念
使用多线程可以提升效率但是也不能牺牲正确性,多线程操作临界区数据时若没有进行恰当的处理,则很有可能会产生冲突。最简单的处理办法就是使用synchronized关键字来实现线程间的同步,更加强大的控制则需要对JUC并发包进行学习。
synchronized关键字除了用于同步,确保线程安全外还可以保证线程间的可见性和有序性。可见性完全可以替代volatile关键字,只是没有volatite使用方便;有序性,被synchronized修饰的代码块内部是串行执行的,必然可以保证有序性。
tips: 并发下的ArrayList可能会抛出下标越界的异常,这是由于ArrayList在扩容过程中,内部一致性遭到破坏,由于没有锁的保护其他线程访问到了不一致的内部状态,导致下标越界。 或者没有异常信息但是最终结果不一致,这是由于多线程访问冲突,使得保存容器大小的变量被不正常访问,同时有多个线程对ArrayList的同一位置进行赋值,并发下的HashMap也可能会出现这个问题。