Thread类
Thread类在java.lang中,实现runable接口。由于java不支持多线程,所以该类至少需要以下功能:
- 作为用户实现的线程类的父类
- 将用户实现的实现runable接口的类的对象转换为Thread对象
- 提供当前代码访问系统线程信息的能力
- 控制当前线程状态
我们都知道,启动线程执行并非是thread.run而是thread.start,这是因为start中调用了
private native void start0();
从而真正开启了线程的执行。每一个线程都属于一个ThreadGroup,可以通过TreadGroup来操作多个线程。
多线程框架
上图中的红线代表相关的关系。
这个极为丑陋的图希望大家能对java构建多线程又一个总体的认识,至少要看懂以下三点:
-
Java多线程的根是Thread类的native start0方法,任何实现都不可能绕开这一点。为了调用这个方法,最原生的做法就是继承thread类,或者实现runable接口,然后用runable对象初始化一个thread对象。在底层,start0方法会使用操作系统fork一个线程出来。具体到linux,一个JVM线程对应一个轻量级进程,而一个轻量级进程对应一个特定的内核线程。这种方式的好处是直接调用内核来调度线程,但是也有缺点,线程执行时需要频繁切换用户态和内核态。
-
Java最原始的Runable接口和JUC中Callable接口的地位是平行的,区别在于一个run方法有返回值,而另一个没有。FutureTask这个类非常重要。负责把callable对象转化成一个runable对象。
-
Executors是Executors框架的核心,负责生成Executor对象,这个对象可以是一个线程,也可以是线程池(ThreadPoolExecutor)。在生成线程池的ThreadPoolExecutor对象时,需要传入一个ThreadFactory对象,这个对象利用Thread类的功能产生新的线程。
工作过程
Java线程池会将提交的任务先置于工作队列中,在从工作队列中获取(SynchronousQueue直接由生产者提交给工作线程)。那么工作队列就有两种实现策略:无界队列和有界队列。无界队列不存在饱和的问题,但是其问题是当请求持续高负载的话,任务会无脑的加入工作队列,那么很可能导致内存等资源溢出或者耗尽。而有界队列不会带来高负载导致的内存耗尽的问题,但是有引发工作队列已满情况下,新提交的任务如何管理的难题,这就是线程池工作队列饱和策略要解决的问题。
饱和策略分为:Abort 策略, CallerRuns 策略,Discard策略,DiscardOlds策略。
- Abort 策略 抛出异常,这是默认策略
- CallerRuns 策略,将这个工作交给放工作进队列的线程来做
- Discard策略,直接丢弃当前要添加的工作。
- DiscardOlds 将队列头(最早被提交但没有开始执行的工作)的工作删除,向队列中添加当前工作
静态方法
- currentThread:获取当前代码的执行线程,由JVM提供实现
public static native Thread currentThread();
- sleep方法:让当前线程进入阻塞状态
public static void sleep(long millis, int nanos)
- yield方法:让当前线程让出cpu,进入就绪状态
public static native void yield();
对象方法:
-
start()方法
开始运行线程。系统会自动执行线程中的run方法。注意,直接调用thread对象的类方法是启动不了线程的 -
getId()
获取线程的唯一标志 -
isAlive()
判断线程是否处于活动状态 -
join()
当前线程等待该线程结束 -
getName和setName
用来得到或者设置线程名称。 -
getPriority和setPriority
用来获取和设置线程优先级。线程的默认优先级和启动该线程的线程相同。注意,JVM不保证线程优先级机制一定有效, -
setDaemon和isDaemon
用来设置线程是否成为守护线程和判断线程是否是守护线程。
守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。