1. 线程相关概念
串行
单线程依次执行每个任务;
并行
开启多个线程多个任务同时执行;
线程
一个进程中任务执行的具体单元,任务执行的最小的单元(CPU调度的最小单元),线程之间资源共享。
进程
在JVM中,启动一个main()方法就是启动了一个进程,进程里又包含了很多线程,线程又叫轻量级进程。进程是对程序运行所占用各种资源的描述(CPU,内存),进程之间资源不共享,是资源分配的最小单元。
线程的生命周期
线程在java中也是一个对象,一个对象的生命周期是指实例化完成到对象销毁的中间过程。
线程类型
- 内核线程(KLT)
线程的所有管理操作由操作系统内核完成,内核通过操作调度器调度线程到内核线程表对线程进行维护,每个内核线程映射到CPU各个处理器。内核线程涉及到用户态和内核态的切换。
JAVA线程创建是依赖于系统内核,通过JVM调用系统库创建内核线程。JVM运行在用户空间,涉及到线程操作依赖底层内核,因此会从用户态切换到内核态。
JVM线程调度流程图:
- 用户线程(ULT)
在用户程序中实现的线程,不依赖操作系统核心。线程创建,管理,调度依赖用户空间的线程库提供的函数,线程的实现系统内核不能感知到,由程序自己完成对线程的管理。
2. 线程的六种状态及状态间的转换
在JDK源码中(java.Lang.Thread),可以看到在Java中给线程定义了六种状态:
2.1 初始化(NEW):
新创建了一个线程对象,但还没有调用start()方法。Thread A = new Thread();
2. 2 运行(RUNNABLE):
Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,此时处于就绪状态(ready),处于就绪状态的线程等待被CPU选中调度,获取CPU的使用权。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。running状态下执行yiled()方法让出JVM资源,该线程又进入就绪状态等待运行。
2.3 阻塞(BLOCKED):
表示线程阻塞于锁。
BLOCKED状态下线程不会释放当前占用的系统资源,进入ready状态后等待CPU选中分配资源执行。
进入BLOCKED状态的条件:
在RUNNING状态下:
等待用户输入;
调用sleep()方法;
调用join()方法;
从BLOCKED回到就绪状态的条件:
用户输入完毕;
等待休眠时间结束;
合并进来的线程执行结束。
2.4 等待(WAITING):
进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
2.5 超时等待(TIMED_WAITING):
该状态不同于WAITING,它可以在指定的时间后自行返回。处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。sleep(超时时间)
2.6 终止(TERMINATED):
表示该线程已经执行完毕。
- 当前线程执行完run()方法或者主线程的main()执行结束,我们就认这个线程终止了。这个线程对象也许是活的,但它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
- 在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
线程状态转换图
前面已经说了初始到运行,运行内部两种状态之间,运行到阻塞,运行到终止这几种状态之间的转换。下面详细介绍一下运行与等待之间的转换过程。
在上面第二图中可以看到,在运行状态下调用wait()方法就会进入WAITING状态,在WAITING状态下线程会经历一个复杂的过程。
进入等待队列
首先,调用wait()方法后实际进入的是等待队列。当发现当前线程需要的资源处于同步(synchronized)状态时,就需要获取对象锁才能使用对象。线程使用完对象锁之后调用wait()方法进入等待队列,处于TIMEWAITING状态;同时当前线程会释放所占有的JVM资源,进入这个状态过后是不能自动唤醒的,必须调用notify()或者notifyAll()方法,线程进入WAITING(同步队列)。
等待队列的操作过程图解:
进入同步队列(锁池)
- 当前线程想调用对象A的同步方法时,发现对象A的锁被别的线程占有,此时当前线程进入锁池。简言之,锁池里面放的都是想争夺对象锁的线程。
- 当一个线程1被另外一个线程2唤醒时,线程1进入锁池,去争夺对象锁。
- 锁池是在同步的环境下才有的概念,一个对象对应一个锁池。
- 线程等待时间到了或被notify/notifyAll唤醒后,会进入锁池竞争锁,如果获得锁标记,进入就绪状态等待被执行,否则进入BLOCKED状态等待获取锁。
3. 线程创建的三种方式
继承Thread类重写run()方法
受单继承限制且子线程没有返回值不能抛出异常
实现Runable接口
Runable接口只有一个run()方法,重写run()方法。可以实现多个接口,开发更加灵活;但子线程没有返回值且不能抛出异常。实现了Runable接口的类要作为创建Thread类的一个参数,最终还是由Thread类实例化线程,调用start()方法。
实现Callable接口
实现Callable接口,重写call()方法,然后包装成java.util.concurrent.FutureTask, 再然后包装成Thread。有返回值,可以抛出异常
线程的执行顺序跟调用start()方法的顺序无关,是CPU随机选取执行的。