JVM线程 / 操作系统线程
- 操作系统的内核级线程, 是CPU资源调度的最小单位;
- 以最简单的时间片轮转调度算法为例, 在定时中断的中断处理函数中, 操作系统的调度程序选出一个就绪线程, 让其上 CPU 运行;
- 另外还有用户级线程, 由用户应用程序内部自己进行切换, 该过程对操作系统来说并不可见, 在操作系统眼里它就是在执行线程 A, 至于应用在线程内自己分离出来并自己负责切换的线程 A-a 和 线程 A-b, 操作系统是不知道的;
- 在HotSpot实现中, 一个 JAVA 线程就是一个内核级线程;
- JVM虚拟机作为一个进程, 创建一个主线程执行main方法;
- 创建线程对象后调用其 start() 方法将创建一个单独的线程, 去执行对应线程对象的 run() 方法, 但是如果直接调用 run() 方法, 会直接在当前线程中执行一次 run() 方法, 不会创建新线程;
- JAVA 线程可以分为用户线程和守护线程, 守护线程用于提供后台常驻的服务或辅助功能,例如垃圾回收线程;
- 当正在运行的线程都是守护线程时, JVM虚拟机将退出;
线程创建
Runnable 与 Thread
- Runnable 定义了可以在单独线程中运行的任务, 仅有一个抽象方法: run( ), 无返回值, 无参数
- Thread类实现了Runnable接口, 同时通过组合的方式,又持有一个Runnable引用 target; Thread类的 run方法的默认实现是调用 target.run( );
- 可以将Runnable的实现类对象传递给Thread类的构造方法, 然后调用该Thread对象的 start() 方法来创建一个线程
new Thread(()->{
System.out.println("lambda");
}).start();
- 也可以直接继承Thread类, 重写其 run()方法, 然后创建Thread 子类对象, 调用其start() 方法来创建线程; 重写run方法以后, 再传入Runnable对象给Thread就不起作用了;
- 不同的Thread对象可以持有相同的Runnable对象, 创建多个不同的线程
- 在 run 方法内还可继续创建线程
Thread类的方法
// 阻塞调用此方法的线程
public static void sleep(long millis);
// 获取当前线程对应的Thread对象
public static native Thread currentThread();
// 放弃当前时间片
public static native void yield();
// 返回当前线程对象对应的线程的状态
public State getState();
// 该方法调用所在的线程会等到调用该方法的线程对象对应的线程执行完毕再执行
// 例如在main方法中调用myThread.join(), 则main线程被阻塞,进入waiting状态, myThead执行完毕后才被唤醒
// 本质上是查询myThread的状态, 如果不是NEW和TERMINATED, 就会循环wait(0);
/* while (isAlive()) {
wait(0); // wait(0)其实就是一直wait;那么为什么要做成while循环?
} */
public void join();
// 如果为 true,则将该线程标记为守护线程。false则标记为用户线程, 一个新创建的线程默认是用户线程
// 该方法必须在启动线程前调用, 否则报java.lang.IllegalThreadStateException
void setDaemon(boolean on);
线程状态
public enum State {
// 创建了Thread对象但没有调用start方法, 没有创建线程
NEW,
// 创建了线程, 线程正常执行
RUNNABLE,
// 线程正在等待获取某个对象的监视器锁时进入阻塞状态, 与之对应的是阻塞队列;
BLOCKED,
// 线程进入等待状态,等待其他线程调用对象的 notify() 或 notifyAll() 方法唤醒
// 与之对应的是等待队列
WAITING,
// 线程进入等待状态,等待一定时间后自动唤醒
TIMED_WAITING,
// 线程执行完毕, 已经销毁
TERMINATED;
}
- BLOCKED与WAITING
- Synchronized获取锁失败时进入BLOCKED状态, 获取到监视器锁后会解除阻塞;
- 线程调用了等待方法(如
Object.wait()
、Thread.join()
、LockSupport.park()
等)进入WAITING状态, 等待其他线程调用对象的notify()
或notifyAll()
方法解除等待; ReentrantLock 获取锁失败的时候, 进入的就是 WAITING 状态
- TIMED_WAITING
当线程调用了一些带有超时参数的等待方法,例如
Thread.sleep(long millis)
、Object.wait(long timeout)
、Thread.join(long millis)
等时,线程会进入TIMED_WAITING
状态。在这种状态下,线程会等待指定的时间,当等待时间到达时,线程会自动被唤醒,然后重新进入就绪状态
Callable接口 与 FutureTask类
- FutureTask类间接实现了Future接口和Runnable接口,
Future
接口代表了异步计算的一种规范, 它提供了检查计算是否完成以及获取运算结果的方法; - Callable是一个泛型类, 有一个抽象方法, call方法, call方法的返回值类型由泛型类型决定
- FutureTask持有一个Callable对象, FutureTask的 run 方法将执行它持有的Callable对象的call方法, 并将结果用自己的成员保存下来;
- 将Callable子类对象传递给FutureTask的构造方法, 再将FutureTask对象传递给Thread构造方法, 就可以创建一个线程了;
- 可以通过FutureTask对象的get方法获取Callable子类对象的call方法的执行结果, get方法是一个阻塞方法
get方法的实现原理是什么?
- 当调用
FutureTask
的get()
方法时,如果任务已经完成(即已经执行完毕),则直接返回执行结果。 - 如果当前的状态是任务尚未完成的状态,当前线程会在get方法内被 LockSupport.park(this) 阻塞, this即FutureTask对象
- 并且会被添加到FutureTask的持有的一个等待链表waiters中;
LockSupport.park(Object)
是Java并发包中LockSupport
类提供的一个静态方法,用于阻塞当前线程,让其进入等待状态,直到调用unpark(Thread)
方法来唤醒该线程- Object对象用来记录阻塞原因, 表示当前线程是因为Object对象导致的阻塞
- 当任务执行完成时,
FutureTask
内部会调用set()
方法将执行结果保存起来。 - 同时在
set()
方法中,会调用LockSupport.unpark()
方法唤醒waiters
队列里因调用get方法被阻塞的线程。 - 需要注意, 调用get方法是在主线程中, 而调用set方法是在FutureTask自己的线程中, 由于任务执行完毕内部调用的;
线程终止
- 首先, 等待线程的 run方法 自然运行结束, 线程就自然终止了;
- 其次, 可以使用
Thread::stop()
方法强制停止某个线程, 但可能导致线程持有的锁和资源还没来得及释放就被终止, 所以不推荐使用;
public class Test1 {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
myThread.start();
Thread.sleep(1000);
// 置标志位
myThread.interrupt();
System.out.println(myThread.getState());
}
}
class MyThread extends Thread{
@Override
public void run() {
// 在while循环的条件判断中调用isInterrupted判断是否需要终止线程
while (!this.isInterrupted()) {
int i = 1;
}
// 这里就可以释放资源了, 所以是优雅的, 可以释放资源的停止方式;
xxx.close();
}
}