线程(Thread)是操作系统能够进行运算调度的最小单位,java线程中创建的、引用的对象在jvm内存中是如何存放的,线程间是如何进行通信的呢,线程发生异常了jvm又是如何处理的呢,接下来让我们从线程的基础知识开始一步一步地了解。
¶线程基础知识
¶什么是线程
线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
¶如何创建线程
- 继承Thread类
1 | /** |
- 实现Runnable接口
1 | /** |
¶线程的状态
Java中的线程一共有六种状态:
- NEW(初始化状态)
- RUNNABLE(可运行 / 运行状态)
- BLOCKED(阻塞状态)
- WAITING(无时限等待)
- TIMED_WAITING(有时限等待)
- TERMINATED(终止状态)
线程可以在这六种状态之间相互转换,如图所示:
¶内存模型
¶java内存模型
- 每一个运行在Java虚拟机里的线程都拥有自己的线程栈,存放当前线程运行的信息。
- 所有原始类型的本地变量都存放在线程栈上,因此对其它线程不可见。
- 所有引用类型的本地变量都存放在堆中,线程栈保存该对象的引用,因此其他线程只要有该对象的引用都可以访问。
- Java程序中无论由哪个对象创建的对象,不管是原始类型对象,还是引用类型对象,都是存放在堆里面。
接下来让我们先看看一段具体代码,这些变量都存放在JVM的什么位置。
1 | public class MemoryModel { |
当test用例执行的时候,各个变量在jvm内存中存放位置如下图所示:
上图中每个线程执行methodOne()
都会在它们对应的线程栈上创建localVariable1
和localVariable2
的私有拷贝。localVariable1
为基础类型对象只存在于线程栈上,localVariable2
为堆内存中Object3的引用。methodTwo方法中的localVariable1都会各自在堆上创建一个对象object1
和object5
,线程栈中存放这两个对象的引用。
执行test用例的时候,我们执行如下两个步骤:
- jmap -dump:format=b,file=./heap_dump.txt 12792
- dump出jvm内存后,使用mat进行分析,可以找到本例中对象的情况
分析结果如下如所示:
¶硬件内存模型
¶java内存模型与硬件内存模型的关系
¶同步异步
同步和异步关注的是:消息通信机制(synchronous communication/ asynchronous communication)。
¶同步(Synchronous)
同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为,如下图所示:
- 打电话
- B/S模式
¶异步(Asynchronous)
异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而,异步方法通常会在另外一个线程中,“真实”地执行着。整个过程,不会阻碍调用者的工作,如下图所示:
- 发短信
- ajax
- 消息队列
¶阻塞和非阻塞
阻塞和非阻塞:强调的是程序在等待调用结果(消息,返回值)时的状态。
阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
¶线程间通信
¶共享对象
线程间发送信号的一个简单方式是在共享对象的变量里设置信号值,如下面代码所示:
1 | private static class MySignal { |
线程A在一个同步块里设置boolean型成员变量hasDataToProcess为true,线程B也在同步块里读取hasDataToProcess这个成员变量。
1 |
|
注意:线程A和B必须获得指向一个MySignal共享实例的引用,否则线程A将收不到信号。
¶wait(),notify()和notifyAll()
通过共享对象,循环检测信号是否被设置,如果没有被设置则进入等待,等待的间隔时间设置过短则对cpu消耗过大,等待的间隔时间设置过长则消息接收不及时。
Java有一个内建的等待机制来允许线程在等待信号的时候变为非运行状态。java.lang.Object 类定义了三个方法,wait()、notify()和notifyAll()来实现这个等待机制。
一个线程一旦调用了任意对象的wait()方法,就会变为非运行状态,直到另一个线程调用了同一个对象的notify()方法。为了调用wait()或者notify(),线程必须先获得那个对象的锁,也就是说,线程必须在同步块里调用wait()或者notify(),否则将抛出java.lang.IllegalMonitorStateException
异常。
1 | @Test |
一旦线程调用了wait()方法,它就释放了所持有的监视器对象上的锁。这将允许其他线程也可以调用wait()或者notify()。一旦一个线程被唤醒,不能立刻就退出wait()的方法调用,直到调用notify()的线程退出了它自己的同步块。换句话说:**被唤醒的线程必须重新获得监视器对象的锁,才可以退出wait()的方法调用,因为wait方法调用运行在同步块里面。**如果多个线程被notifyAll()唤醒,那么在同一时刻将只有一个线程可以退出wait()方法,因为每个线程在退出wait()前必须获得监视器对象的锁。
¶异常处理
¶Thread默认的异常处理
线程都不允许抛出未捕获的checked exception(比如sleep时的InterruptedException
),也就是说各个线程需要自己把自己的checked exception处理掉。我们可以查看一下Thread类的run()方法声明,方法声明上没有对抛出异常进行约束。
1 | //Thread类中 |
线程是独立执行的代码片断,线程的问题应该由线程自己来解决,而不要委托到外部。
¶未捕获的异常去哪儿了
一个异常被抛出后,如果没有被捕获处理,则会一直向上抛。异常一旦被Thread.run() 抛出后,就不能在程序中对异常进行捕获,最终只能由JVM捕获。
1 |
|
¶JVM如何处理线程中抛出的异常
查看Thread类的源码,我们可以看到有个dispatchUncaughtException方法,此方法就是用来处理线程中抛出的异常的。JVM会调用dispatchUncaughtException方法来寻找异常处理器(UncaughtExceptionHandler),处理异常。
1 | // 向handler分派未捕获的异常。该方法仅由JVM调用。 |
UncaughtExceptionHandler必须显示的设置,否则默认为null。若为null,则使用线程默认的handler,即该线程所属的ThreadGroup。ThreadGroup自身就是一个handler,查看ThreadGroup的源码就可以发现,ThreadGroup实现了Thread.UncaughtExceptionHandler接口,并实现了默认的处理方法。默认的未捕获异常处理器处理时,会调用 System.err 进行输出,也就是直接打印到控制台了。
1 | public void uncaughtException(Thread t, Throwable e) { |
¶产考文献
- http://ifeve.com/java-concurrency-thread-directory/
- http://tutorials.jenkov.com/java-concurrency/index.html
- https://fanzhongwei.com/thread/h5/thread.html
- 《深入Java虚拟机:JVM高级特性与最佳实践(第2版)》