基本实现
在java中,Java官方提供了三种方式来帮助我们实现一个线程,其中:
第一种方式:继承 Thread 对象:extends Thread
// 自定义线程对象
class ApplicationThread extends Thread {
public void run() {
// 线程需要执行的代码
......
}
}
其中,Thread 类本质上是实现了Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方
法就是通过Thread 类的start()实例方法。start()方法是一个native 方法,它将启动一个新线程,并执行run()方法。
第二种方式:实现 Runnable 接口(无返回值):implements Runnable
// 实现Runnable接口
class ApplicationThread implements Runnable {
@Override
public void run() {
// 线程需要执行的代码
......
}
}
其中,如果自己的类已经extends 另一个类,就无法直接extends Thread,此时,可以实现一个Runnable 接口。
第三种方式:实现Callable 接口(有返回值):implements Callable
// 实现Runnable接口
class ApplicationThread implements Callable {
@Override
public void run() {
// 线程需要执行的代码
......
}
}
其中,执行Callable 任务后,可以获取一个Future 的对象,在该对象上调用get 就可以获取到Callable 任务返回的Object对象。
第四种方式:基于线程池方式创建:线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。
Java 里面线程池的顶级接口是Executor,但是严格意义上讲Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
Java主要提供了newCachedThreadPool,newFixedThreadPool,newScheduledThreadPool以及newSingleThreadExecutor 等4种线程池。
目前业界线程池的设计,普遍采用的都是生产者 - 消费者模式。线程池的使用方是生产者,线程池本身是消费者。
Java 并发包里提供的线程池,比较强大且复杂。Java 提供的线程池相关的工具类中,最核心的是 ThreadPoolExecutor,通过名字你也能看出来,它强调的是 Executor,而不是一般意义上的池化资源。
ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueueworkQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
对于这些参数的意义,我们可以把线程池类比为一个项目组,而线程就是项目组的成员。其中:
corePoolSize:表示线程池保有的最小线程数。
maximumPoolSize:表示线程池创建的最大线程数。
keepAliveTime & unit:一个线程如果在一段时间内,都没有执行任务,说明很闲,keepAliveTime 和 unit 就是用来定义这个“一段时间”的参数。也就是说,如果一个线程空闲了keepAliveTime & unit这么久,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收。
workQueue:工作队列。
threadFactory:通过这个参数你可以自定义如何创建线程名称。
handler:通过这个参数你可以自定义任务的拒绝策略。
其中,Java在ThreadPoolExecutor 已经提供了以下 4 种策略:
CallerRunsPolicy:提交任务的线程自己去执行该任务
AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException
DiscardPolicy:直接丢弃任务,没有任何异常抛出
DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列
同时, Java 在 1.6 版本还增加了 allowCoreThreadTimeOut(boolean value) 方法,表示可以让所有线程都支持超时。
调度方式
由于CPU的计算频率非常高,每秒计算数十亿次,因此可以将CPU的时间从毫秒的维度进行分段,每一小段叫作一个CPU时间片。
目前操作系统中主流的线程调度方式是:基于CPU时间片方式进行线程调度。
线程只有得到CPU时间片才能执行指令,处于执行状态,没有得到时间片的线程处于就绪状态,等待系统分配下一个CPU时间片。
由于时间片非常短,在各个线程之间快速地切换,因此表现出来的特征是很多个线程在“同时执行”或者“并发执行”。
在Javs多视程环境中,为了保证所有线程都能按照一定的策略执行,JVM 需要有一个线程调变器支持工作。
这个调度器定义了线程测度的策略,通过特定的机制为多个线分配CPU的使用权,线程调度器中一般包含多种调度策略算法,由这些算法来决定CPU的分配。
除此之外,每个线程还有自己的优先级(比如有高,中、低级别)调度算法会通过这些优先级来实现优先机制。
常见线程的调度模型目前主要分为两种:(分时)协同式调度模型和抢占式调度模型。
抢占式调度:
系统按照线程优先级分配CPU时间片
优先级高的线程优先分配CPU时间片,如果所有就绪线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些。
每个或程的执行时间和或候的切换高由调度落控划,调度器按照某种略为每个线穆分配执行时间,
调度器可能会为每个线整样分配相的执行时间,也可能为某些特定线程分配较长的执行时间,甚至在极准情况下还可能不给某热线程分!执行时同片,从而导致某技线相得不到执行,
在抢占式调支机制下,一个线程的堵事不会导致整个进程堵客
(分时)协同式调度:
系统平均分配CPU的时间片,所有线程轮流占用CPU,即在时间片调度的分配上所有线程“人人平等”。
某一线相执行完后会主动通知调度器切换现下一个线程上继续执行。
在这种模式下,线程的执行时间由线程本身控物,也就是说线程的切换点是可以预先知道的。
在这种模式下,如果某个钱程的逻辑辑存在问题,则可能导致系统运行到一半就阻塞了,最终会导致整个进程阻塞,甚至更糟可能导致整个系统崩溃。
由于目前大部分操作系统都是使用抢占式调度模型进行线程调度,Java的线程管理和调度是委托给操作系统完成的,与之相对应,Java的线程调度也是使用抢占式调度模型,因此Java的线程都有优先级。
主要是 因为Java的线程调度涉及JVM的实现,JVM规范中规定每个线程都有各自的优先级,且优先级越高,则越优先执行。
但是,优先级越高并不代表能独占执行时间,可能优先级越高得到的执行时间越长,反之,优先级越低的线程得到执行时间越短,但不会出现不分配执行时间的情况。
假如有若干个线程,我们想让一些线程拥有更多的执行时间或者少分配点执行时间,那么就可以通过设置线程的优先级来实现。
所有处于可执行状态的线程都在一个队列中,且每个线程都有自己的优先级,JVM 线程调度器会根据优先级来决定每次的执行时间和执行频率。
但是,优先级高的线程一定会先执行吗?我们能否在 Java 程序中通过优先级值的大小来控制线程的执行顺序呢?
答案是肯定不能的。主要是因为影响线程优先级语义的因素有很多,具体如下:
不同版本的操作系统和 JVM 都可能会产生不同的行为
优先级对于不同的操作系统调度器来说可能有不同的语义;有些操作系统的调度器不支持优先级
对于操作系统来说,线程的优先级存在“全局”和“本地”之分,不同进程的优先级一般相互独立
不同的操作系统对优先级定义的值不一样,Java 只定义了 1~10
操作系统常常会对长时间得不到运行的线程给予增加一定的优先级
操作系统的线程调度器可能会在线程发生等待时有一定的临时优先级调整策略
JVM 线程调度器的调度策略决定了上层多线程的运行机制,每个线程执行的时间都由它分配管理。
调度器将按照线程优先级对线程的执行时间进行分配,优先级越高得到的 CPU执行时间越长,执行频率也可能更大。
Java把线程优先级分为10个级别,线程在创建时如果没有明确声明优先级,则使用默认优先级。
Java定义了 Thread.MIN_PRIORITY、Thread.NORM PRIORITY和 Thread.MAXPRIORITY这3个常量,分别代表最小优先级值(1)、默认优先级值(5)和最大优先级值(10)。
此外,由于JVM 的实现是以宿主操作系统为基础的,所以Java各优先级与不同操作系统的原生线程优先级必然存在着某种映射关系,这样才能够封装所有操作系统的优先级来提供统一的优先级语义。
一般情况下,在Linux中可能要与-20~19之间的优先级值进行映射,而Windows系统则有9个优先级要映射。
更多关于“java培训”的问题,欢迎咨询千锋教育在线名师。千锋教育多年办学,课程大纲紧跟企业需求,更科学更严谨,每年培养泛IT人才近2万人。不论你是零基础还是想提升,都可以找到适合的班型,千锋教育随时欢迎你来试听。