多线程_入门级
前言:本篇,只是简简简简简简简简单的入门而已……
因为确实短时间不可能掌握多线程原理
阅读本博文预计耗时 20-30分钟
前置概念
入门级概念
并发
并发:是定义在操作系统上的概念
指在某一个时间片段中,有多个进程都处于启动状态—(就绪,运行,阻塞)—退出状态之间,且这几个进程都是在同一个CPU(Control Processer Unit)上运行,但任一个时刻只有一个进程在CPU上运行。
举例一段时间:||||||||
——这是一个时间片段,由8个时刻组成。
一般来讲,时刻的单位是1Hz,代表每秒中的周期性变动重复次数的计量
CPU的处理能力是 1GHz ≈ 1000MHz ≈ 106KHz ≈ 109 Hz
也就是……emmm这个东西很强很猛,1s处理109个的时刻【这个说法不严谨】
大概就是这个意思,也就是为什么说,在某时刻只有一个,但我们感觉不出来,还是觉得多个进程是同时完成的,是因为CPU处理的太快了,你不觉得它们是间断交叉执行的。
和视觉残留
的感觉是差不多的。不是因为你眼前还有实物(不是程序从未中断运行),而是你的记忆中还有实物的影子(而是多个程序 “左右横跳” 晃瞎你了你,令你觉得它们都还在运行队列)。
流水线
当然,看到这里会觉得有些模糊,但又觉得知道了什么。
那么在深究一点:CPU为什么能够在一个时间段执行多个进程?
因为CPU这个物体实体内部,做了工业流水线的设计(实际的物理结构决定了并发功能)
什么是流水线?
在经济学历史上有一个人很厉害——科学管理之父·弗雷德里克·温斯洛·泰勒
泰勒最牛逼的成就是什么?——提高工业生产效率,他是如何提高效率的?
很简单,把生产过程作为一个整体,进行切割,然后进行分析,然后对每个部分提升效率
最终达到了整体生产效率大幅度提高。
这个成果可以用批评他的人一句话来展示:“辛克莱的年轻的社会主义者写信给《美国杂志》主编,指责泰勒“把工人的工资提高了61%,但工作量却提高了362%”。”
没错,这就是万恶的资本主义,哈哈哈哈哈哈。这也引出了流水线的核心目标
不让任何一个人空闲1秒钟,要让他工作的8个小时都在干活
不让CPU的任何一个计算单元空闲1毫秒,要让它无休无止的干活
如果还是觉得流水线很模糊,那就请了解一下历史上的福特汽车,以超低售价霸占美国汽车销售市场,采用流水线作业。
这里穿插一个分析,流水线的核心价值来源——生产效率与事件的复杂程度成反比。
就人类而言,越简单的事情执行起来越高效,越复杂的事情执行起来效率会越低。
例如,一个人只劈柴,一天可以劈1200根木头,只收集木头,一天可收集400根,如果又劈柴又收集木头只能处理200根,那么现在有4个人,怎么效率更高?
效率最低的是4个人自己干收集木头
+劈柴
整个流程,1天内4人整体可处理800根
效率最高的是分工合作,流水线作业。3个人只收集木头共1200根,1个人只劈柴共1200根,1天内4人整体效率提升了1200/800=150%
所以,我们尽可能的将一个复杂事情拆分成几个简单的事情。以通过和他人分工合作的方式来提高生产效率(时间利用率)。【是的,我们所谓效率是以产出与时间对比而来的】
CPU流水线
那么接下来,我们讨论CPU流水线设计的具体结构
为什么我们的CPU作为一个整体可以拆分呢?
其实这个问题在某个程度上是不正确的,但CPU确实也是可以拆分的,由多个部分组成
但真正拆分的是线程命令转换成的汇编指令在CPU中的运行过程(类比生产过程)。
我们来解析一下,汇编指令是如何被CPU运行的
JAVA代码编译为二进制码,然后被JVM转换为机器汇编指令,然后存储在存储器中。
这个时候——CPU开始工作
CPU从存储器中取出指令,然后将取出的指令进行翻译,得到执行指令的各种前置信息。然后对指令进行执行,执行过程中可能发现需要存储器(内存)中的数据,那么就需要访存,当计算完毕后,将结果写回存储器,好了CPU负责的整个流程就结束了。
我们可以从这个过程中抽出5个关键步骤:1.取指,2.译码,3.执行,4.访存,5.写回
嗯,但这又怎么做到像造汽车一样的流水线作业呢?
原本的CPU是一整块结构,一个挨着一个执行,A|B|C|D|E
A部分执行完了,电流跑到B部分,最后一直跑到E部分,跑完整个流程。但这个时候你会发现,电流跑到C的时候,A、B部分都闲下来了!!不行,我们不能让它们闲着
现在我们可以将整体分成为5个部分,让取值的部分只取值,再给它配个存储器,让译码的只译码,也给它配个存储器,等等,如此操作就会有这样的效果,虽然仍然是一个时刻只有一个线程在运行EX,但好像我在一段时间内跑了好多个线程也不觉得有线程被中断执行!!!【在一个时间段内,多个线程处于启动状态—(就绪,运行,阻塞)—退出状态之间】
这里也就是所谓1核CPU=1个线程
超线程技术
顺便说一下为什么现在的Inter是4核8线程,8核16线程,12核24线程。
实话实话,8核还是实质是8线程,只是因为Inter的一核有10-25级流水线,但通常情况下,功能过剩,可能只运行到13级就完成了。那么为了充分利用这么多级流水线,英特尔在一个实体CPU中,提供两个逻辑线程【只有逻辑线程,但CPU不止由逻辑线程组成】,在CPU内部仅复制必要的资源、让两个线程可同时运行;在一单位时间内处理两个线程的工作,模拟实体双核心、双线程运作。然后利用上未被使用的其他级流水线。这个技术叫做超线程技术。通常超线程技术能够提升只有1个线程运行的情况下的15-30%的工作效率。
这个东西我…不是很清楚,有兴趣的自行寻找。【感觉就是,把CPU组成线程的核心部件增加1个,然后复用原本一个线程的资源部件,一个身体两个头?双头蛇?】
好了,到此,关于并发应该就没有任何疑问了。都有了宏观概念及实物对应。多思考思考理解核心。
参考文献:
http://m.elecfans.com/article/657563.html
https://baike.baidu.com/item/%E8%B6%85%E7%BA%BF%E7%A8%8B
并行
当你完完全全的将并发看懂了之后,对于并行理解起来,只需要1分钟的时间
并行就是多个CPU……哈哈哈哈,对的,4核CPU4线程,同时执行4个线程
注意是:同时同时同时!!!!
当然,并不是核心越多越好,因为,人多嘴杂,管理及交流成本太高,以及你CPU就屁大点地方,如果离的太远,会导致核心间通信的耗时比计算耗时还要久(面积≈距离与速度≈性能之比),还要考虑散热(CPU耗能比很高,使用的电流90%发散为焦耳热)等等
所以,并行很厉害,但并发也很重要,而且二者是可以叠加奏效的。
你强,我也强,我们联合更强。大概是这样的意思
********
程序
一般是指的,未被启动的静态EXE文件。
程序本质:是指令、数据及其组织形式的描述
进程
指计算机中已启动并运行的程序。
在面向进程设计的系统(如早期的UNIX,Linux 2.4及更早的版本)中,进程是程序的基本执行实体;
在面向线程设计的系统(如当代多数操作系统、Linux 2.6及更新的版本)中,进程本身不是基本运行单位,而是线程的容器。
进程是一个动态的实体(包含程序执行环境/上下文的实体),它拥有自己的生命周期。是通过程序被OS启动,实例化创建在内存的中的,程序的一个实体/线程依存的一个容器。
一个进程中可以并发多个线程,至少有一个线程,每条线程可并行执行不同的任务(顺序代码)。
也可简单的认为:进程 = CPU加载上下文(就绪)+CPU执行+CPU保存上下文(挂起)
若干进程有可能与同一个程序相关系,例如双开DNF游戏客户端,2个进程同1个程序。
进程内容
- 程序的可执行机器代码在存储器的映像。
- OS分配的存储器。存储器的内容包括可执行代码、特定于进程的数据(输入、输出)、永久堆栈、临时堆栈。
- OS分配的资源描述符,诸如文件描述符(Unix术语)或文件句柄(Windows)、数据源和数据终端、进程PID。
- 安全特性,诸如进程拥有者和进程的权限集(可以容许的操作)。
- 处理器状态(内文),诸如寄存器内容、物理存储器定址等。当进程正在运行时,状态通常存储在寄存器,其他情况在存储器。
- 一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
进程状态
新生(new):进程新产生中。
运行(running):正在CPU中执行指令。
等待(waiting):等待某事发生,例如等待用户输入完成。亦称“阻塞”(blocked)
就绪(ready):排队中,等待CPU执行。
结束(terminated):完成运行。
线程
线程是操作系统能够进行运算调度的最小单位。真正在CPU上运行的是线程。
它被包含在进程之中,是进程中的实际运作单位。
同一进程中的多条线程将共享该进程中的被分配到的资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。**但同一进程中的多个线程又有独立的系统资源(当然是从进程中划拨过来的)**如调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
线程与进程比较
1.线程节省了切换进程上下文,以及进程间交流的开销。
因为它们共享一个进程的全部系统资源,可直接访问共享变量。
2.进程管理OS分配给程序实例的资源,及管理它下面的多线程,线程管理如何进行计算/执行任务(让专业的人做专业的事情来提高整体效率/Unix哲学:做好一件事)
3.进程是资源分配的最小单位,线程是CPU调度的最小单位(如果想要增大系统资源,创建子进程,如果想要增加工作效率开辟子线程,当然是子线程是使用父进程的系统资源)
4.进程的执行过程是线状的,尽管中间发生中断暂停,但该进程所拥有的资源只为该线状执行过程服务。一旦发生进程上下文切换,这些资源都是被保护起来的,不被别人使用。
5.线程的改变只代表了 CPU 执行过程的改变,而没有发生进程所拥有的资源变化。
6.计算机内的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。
7.进程拥有一个完整的虚拟地址空间,不依赖于线程而独立存在;反之,线程是进程的一部分,没有自己的地址空间,与进程内的其他线程一起共享分配给该进程的所有资源。
8.线程中执行时一般都要使用同步(synchronized)和互斥,因为他们共享同一进程的所有资源。
5.6.7.8.9来源
知乎作者:力扣(LeetCode)
分类
Unix定义的线程
用户级线程
用户级线程不需要经过用户态/核心态切换,速度快,但操作系统内核不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所有线程)阻塞。由于这里的处理器时间片分配是以进程为基本单位,所以每个线程执行的时间相对减少。虽然少了进出内核态的消耗,但不能很好的利用多核Cpu。
应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。
内核级线程
切换由内核控制,当线程进行切换的时候,由用户态转化为内核态。切换完毕要从内核态返回用户态;可以很好的利用smp,即利用多核cpu。windows线程就是这样的。
一个内核线程由于I/O操作而阻塞,不会影响其它线程的运行。内核级线程又称为内核支持的线程或轻量级进程
二者区别
1.用户级线程的创建、撤消和调度不需要OS内核的支持,是在语言(如Java)这一级处理的;而内核支持线程的创建、撤消和调度都需OS内核提供支持,而且与进程的创建、撤消和调度大体是相同的。
2.内核支持线程是OS内核可感知的,而用户级线程是OS内核不可感知的。
用户进程优点
(1) 线程的调度不需要内核直接参与,控制简单。
(2) 可以在不支持线程的操作系统中实现。
(3) 创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多。
(4) 允许每个进程定制自己的调度算法,线程管理比较灵活。
(5) 线程能够利用的表空间和堆栈空间比内核级线程多。
(6) 同一进程中只能同时有一个线程在运行,如果有一个线程使用了系统调用而阻塞,那么整个进程都会被挂起。另外,页面失效也会产生同样的问题。
用户进程缺点:
(1)资源调度按照进程进行,多个CPU,同一个进程中的线程也只能在同一个处理机下分时复用
Java线程分类
守护线程
守护线程又称为服务线程,主要功用是——服务其他非守护线程即用户线程。
只要当前JVM实例中尚存任何一个非守护线程没有结束,守护线程就全部工作;
只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。
守护线程并非只有虚拟机内部提供,用户在编写程序时也可以自己设置守护线程。
参考博文 ziq711
用户线程(非守护线程)
用户自己使用Thread类创建的线程
Java 多线程如何实现在多 CPU 上分布?
根据JVM虚拟机来确定实现方法。常用SE JVM是交给OS来进行选择。
常用的JVM实现,Oracle/Sun的HotSpot VM,它是用1:1模型来实现Java线程的,也就是说一个Java线程是直接通过一个OS线程来实现的,中间并没有额外的间接结构。而且HotSpot VM自己也不干涉线程的调度,全权交给底下的OS去处理。所以如果OS想把某个线程调度到某个CPU/核上,它就自己弄了。
线程状态
与进程相同
1.新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
2.就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
3.运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
4.阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
-
等待阻塞:
运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。 -
同步阻塞:
线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。 -
其他阻塞:
通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
5.死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
生命周期
线程池
类比数据库连接池
线程池可以看做是线程的集合。在没有任务时线程处于空闲状态,当请求到来:线程池给这个请求分配一个空闲的线程,任务完成后回到线程池中等待下次任务**(而不是销毁)。这样就实现了线程的重用。
主要功能:减少创建销毁Thread对象的时间开销,以及创建Thread对象的延迟。
线程生命周期的开销非常高。创建和销毁线程所花费的时间和资源可能比处理客户端的任务花费的时间和资源更多,并且还可能会出现空闲线程占用系统资源。
以下为JDK的线程池相关库
相关参考 知乎@Java3y
********
进阶级概念
JVM
Synchronized【本博客有2篇相关文章(入门)】
Volatile
Java线程相关库(含实现)
Thread,Runnable,Callable,Future,FutureTask,Executer
Thread(简)
Thread类是Java实现多线程的基本类,Thread类 实现 Runnable接口 void run()
值得一提,Java应用程序调用main()方法也是由一个非守护线程来执行的。
新的thread的优先级初始设置为创建新线程的线程的优先级,并且当且仅当创建新线程的线程是守护进程时才是守护进程线程。
函数一览
常用实例方法
1 public void start()
使该线程开始执行;Java 虚拟机调用该线程的 run 方法。【准备进程上下文】
2 public void run()
如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。【也就是必须实现Runnable接口】
3 public final void setName(String name)
改变线程名称。
4 public final void setPriority(int priority)
更改线程的优先级。
5 public final void setDaemon(boolean on)
将该线程标记为守护线程(true)或用户线程(false)。
6 public final void join(long millisec)
等待该线程终止的时间最长为 millis 毫秒。
7 public void interrupt()
中断线程。
8 public final boolean isAlive()
测试线程是否处于活动状态。
构造方法
Thread 定义了多个构造方法,下面是常用的:
Thread(Runnable threadOb,String threadName);
threadOb:实现Runnable接口的实现类对象,threadName定义线程名
//常用构造方法
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
/**
* 与上面的null,null,"Thread-" + nextThreadNum()具有同样效果
*
* 自动生成的名称具有以下形式
* “Thread - ”+ n,其中n是整数
*
* @param target
* 启动此线程时调用run()的对象。 如果Target=null,则此Thread.run()不执行任何操作。
**/
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
常用实例方法
序号 | 方法描述 |
---|---|
public void start() | 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 |
public void run() | 如果线程使用 Runnable 实现类构造,则调用该 Runnable 对象 run 方法;否则,该方法不执行任何操作并返回。 |
public final void setName(String name) | 改变线程名称,使之与参数 name 相同。 |
public final void setPriority(int priority) | 更改线程的优先级。 |
public final void setDaemon(boolean on) | 将该线程标记为守护线程或用户线程。 |
public final void join(long millisec) | 等待该线程终止的时间最长为 millis 毫秒。 |
public void interrupt() | 中断线程。 |
public final boolean isAlive() | 测试线程是否处于活动状态。 |
常用静态(类)方法
方法 | 方法描述 |
---|---|
public static void yield() | 暂停当前正在执行的线程对象,并执行其他线程。 |
public static void sleep(long millisec) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
public static boolean holdsLock(Object x) | 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。 |
public static Thread currentThread() | 返回对当前正在执行的线程对象的引用。 |
public static void dumpStack() | 将当前线程的堆栈跟踪打印至标准错误流。 |
构造Thread的三种方法
传入以下三种实例进入Thread类构造器中
①Runnable实现类
- 实现 Runnable 接口的实现类;【相较继承无限制,使用方便】【但没有返回值】
②继承Thread子类
- 继承 Thread 类的子类;【与Runnable没本质区别,Thread也实现了Runnable】
③Callable&Future实现类
- 实现 Callable 和 Future接口的实现类(二次封装的FutureTask类)【有返回值】
*3
继承Thread类
使用方法
继承类必须重写 run() 方法,run()是新线程的入口点。继承类通过调用Thread.start() 方法执行。
实例演示
1.class 新进程1 extends Thread
2.Override run方法
public class SampleThread2 extends Thread{
@Override
public void run() {
}
}
run() 内可以调用其他方法,使用其他类,并声明变量,就像主线程一样。
注:创建一个实现 Runnable 接口的类之后,你可以在类中实例化一个线程对象。这样就可以封装Thread方法。如下
class SampleThread2 extends Thread {
private Thread t;
@Override
public void run() {
}
//SampleThread类的start方法封装Thread.start()方法
public void start () {
System.out.println("Starting " + threadName );
if (t == null) {
t = new Thread (this, threadName);
t.start ();
}
}
}
3.启动线程
public class TestThread {
public static void main(String args[]) {
//未封装Thread类
SampleThread2 sampleThread = new SampleThread2();
Thread A_thread1 = new Thread(sampleThread, "A_thread1");
A_thread1.start();
}
}
Thread类Q&A
Q1:为什么要继承/实现接口,而不是直接实例化Thread?
A:因为Thread中的run方法是空的,需要程序员复写。
Q2:run方法和调用start方法区别?
A:run方法是执行体,start方法是使目标线程进入就绪状态。
Q3:为什么要新建线程?
A:多线程允许执行多个顺序代码单独执行,而不是都在main方法中进行同步阻塞执行。
Q4:为什么线程执行具有随机性?
A:启动一个线程并不会立即执行,而是等待CPU的资源调度,CPU能调度哪个线程,是通过多种复杂的算法计算而来,故在不精通OS的程序员看来线程是随机执行的。当然我们也可以在一定程度上控制线程执行顺序。【线程优先级属性控制,或者线程池控制,或者join函数,暂停当前主函数,执行子函数。哈哈哈,太蠢了】
Q5:就绪的子线程与父线程有区别吗
A:每一个执行线程与父线程一样,都有自己独有的栈内存空间,进行方法的压栈和弹栈。
Runnable
无返回值的线程执行
源码
public interface Runnable {
public abstract void run();
}
当启动一个线程后会自动调用复写的run方法,run方法在一般契约下被允许做任何事情。
实现Runnable接口:
实现Runnable接口的类重写 run() ,该方法是新线程的入口点。
新线程类必须通过调用Thread.start() 方法才能被执行。
实例演示
Runnable与继承Thread类一模一样……
1.类 新线程1 implements Runnable
2.@Override run方法
public class SampleThread implements Runnable{
//重点!!!run方法没有返回值!!!
@Override
public void run() {
}
}
注:run() 内可以调用其他方法,使用其他类,声明变量,同主线程一样【do everything】
注:创建一个实现 Runnable 接口的类之后,你可以在类中实例化一个线程对象。这样就可以封装Thread方法。如下
class SampleThread implements Runnable {
private Thread t;
//SampleThread类的start方法封装Thread.start()方法
public void start () {
System.out.println("Starting " + threadName );
if (t == null) {
t = new Thread (this, threadName);
t.start ();
}
}
}
3.启动线程
public class TestThread {
public static void main(String args[]) {
//未封装Thread类
SampleThread sampleThread = new SampleThread();
//传入Runnable实例构建Thread实例
Thread A_thread1 = new Thread(sampleThread, "A_thread1");
//启动Thread实例
A_thread1.start();
}
}
Callable&Future
有返回值的线程执行
Callable接口源码
public interface Callable<V> {
V call() throws Exception;
}
Future接口源码
public interface Future<V> {
//取消任务(可能不会被取消)
boolean cancel(boolean mayInterruptIfRunning);
//查询任务是否被取消(如果此任务在正常完成之前被取消,则返回true)
boolean isCancelled();
//查询任务是否已完成(如果此任务已完成,则返回true)
boolean isDone();
//等待计算过程,然后获取计算结果
V get() throws InterruptedException, ExecutionException;
//等待规定时间,若超时抛出TimeoutException,否则返回计算结果
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
实现C&F
与Runnable的使用方法没太大区别,只是Callable多了返回值类型,而Future接口是增强了对于线程返回结果的操作(取消任务、查询任务状态、获取结果可延时)。
实例演示
1.创建 Callable 接口的实现类,复写 call方法, call()方法作为线程执行体,有返回值
【类比Runnable接口的实现类,复写 run方法,run()方法作为线程执行体,无返回值】
2.new Callable 实现类的实例,使用 FutureTask 类(Future接口实现类) 包装 Callable 对象
【该 FutureTask 对象封装 Callable实现类 对象的 call() 方法的返回值】
注意:FutureTask也能封装Runnable接口,当Runnable实例成功执行后,返回result。
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
/**
* @param可运行的任务=Runnable实例
* @param执行成功完成后返回result。
* 如果您不需要特定结果,请考虑使用表单的结构:
* {@code Future <?> f = new FutureTask <Void>(runnable,null)}
*
**/
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
3.使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
【Runnable是直接以Runnable实现类实例作为Thread对象的target,没有二次封装】
注意:Future接口与Runnable接口没半毛钱关系,但Thread的构造函数接受的参数要么是Runnable,要么是ThreadGroup线程池,或者String。故需要FutureTask,它实现了Runnable及Future接口。
4.调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
【Runnable没有返回值】
简单实例
//1.Callable<Integer>接口实现类,不过这个<Integer>不写也没问题
public class SampleCallableThread implements Callable<Integer> {
@Override
public Object call() throws Exception {
return 1;
}
}
//测试类
public class ThreadTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//2.实例化Callable实现类
SampleCallableThread sampleCallableThread = new SampleCallableThread();
//3.使用FutureTask类实例封装Callable实现类对象(显式指定返回值类型与Callable实现类的返回值相同)
FutureTask<Integer> integerFutureTask = new FutureTask<Integer>(sampleCallableThread);
//4.传入FutureTask二次封装对象新建Thread实例,并启动线程Thread
new Thread(integerFutureTask, "SampleCallableThread").start();
System.out.println(integerFutureTask.get());
}
}
稍微复杂实例
//1.Callable<Integer>接口实现类
public class SampleCallableThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int i = 0;
for (; i < 10; i++)
System.out.println(Thread.currentThread().getName() + " 的循环变量i的值" + i);
//返回i=10
return i;
}
}
//测试类
public class ThreadMain {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//2.实例化Callable<Integer>接口实现类
SampleCallableThread sampleCallableThread = new SampleCallableThread();
//3.实例化FutureTask<Integer>类(Future接口的实现类)
//以Callable<Integer>接口实现类作为FT类构造函数的参数完成FT类实例初始化
FutureTask<Integer> integerFutureTask = new FutureTask<Integer>(sampleCallableThread);
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " 的循环变量i的值" + i);
//当i==20时,新建进程Thread(以FT实例作为Target启动)
if (i == 20)
//4.传入FT实例构建Thread实例,并启动Thread线程。
new Thread(integerFutureTask, "SampleCallableThread").start();
}
System.out.println("子线程的返回值:"+integerFutureTask.get());
}
}
FutureTask
可取消的异步计算。该类提供了Future的基本实现,提供了启动和取消计算、查询是否完成计算以及检索计算结果的方法。
计算完成后才可检索结果;如果计算尚未完成, get方法将阻塞。一旦计算完成,就不能重新启动或取消计算(除非使用runAndReset调用计算)。
注1:FutureTask也可二次封装Runnable实例,不仅可封装Callable实例。返回值是由构造时传入参数(可指定的),然后通过适配器Adaptor类进行处理。
注2:因为FutureTask实现了Runnable,所以可将FutureTask提交给Executor线程池执行。
定制化Future任务类
FutureTask还拥有4个Protected函数,可帮助程序员对任务进行定制化
/**
* 当此任务转换为完成状态时调用的方法(无论是正常完成还是经过取消)。
* 默认实现:什么都不做。
* 子类可以重写此方法以调用完成回调或执行簿记。
* 可以在此方法内部实现中查询任务状态,以确定是否已取消此任务。
**/
protected void done() { }
/**
* 将此任务计算得到的结果设置为给定值,除非该结果已经被设置或已被取消。
**/
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}
/**
* 设置抛出的计算过程中抛出的异常信息。
**/
protected void setException(Throwable t) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = t;
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
finishCompletion();
}
}
/**
* 在不设置结果的情况下执行计算,然后将这个future重置为初始状态
* 如果计算遇到异常或被取消则无法执行此操作。
* 当重新执行计算并成功运行后,返回true
**/
protected boolean runAndReset() {
...;
}
Executor
Executor接口提供了一种方法,可以将任务提交与每个任务的运行机制(包括线程使用、调度等细节)分离开来。
public interface Executor {
//一般实现是在未来某个时刻,执行Runnable接口的run方法
//Executes the given command at some time in the future.
//因为不是立刻执行,故称为 任务提交 ——》将执行体run()建立线程
//进入线程的就绪状态,等待CPU调用进入线程的运行状态。
void execute(Runnable command);
}
Executor向下继承关系
ForkJoinPool
ForkJoinPool是Fork/Join框架的两大核心类之一。与其它类型的ExecutorService相比,其主要的不同在于采用了工作窃取算法(work-stealing):所有池中线程会尝试找到并执行已被提交到池中的或由其他线程创建的任务。这样很少有线程会处于空闲状态,非常高效。这使得能够有效地处理以下情景:大多数由任务产生大量子任务的情况;从外部客户端大量提交小任务到池中的情况。
jdk1.7新增实现类
ThreadPoolExecutor
线程池可以类比数据库连接池,功能与存在意义都是相似的,只不过是服务的对象从数据库连接对象,变成了线程对象。
意义:重用,减少new对象与destroy对象的时间开销。
额外作用:提供管理、限定线程资源的方法
实现:当需要新建线程时,线程池给这个请求分配一个空闲的线程,Runnable实例的run()执行体内容完成后,Thread线程对象回到线程池中等待下次任务,而不是直接销毁线程。
与Executor的关系
ThreadPoolExecutor实现了Executor的execute()方法
概述
创建线程池
ThreadPoolExecutor类 拥有众多属性,也就是——可以线性组合出多种适合不同使用需求的线程池。也就是——这个线程池类是拥有多种策略的集合体。
你可以根据需求自定义线程池不同方面的属性(通过构造函数参数设置/继承这个类),以来满足你独特♂的需求。
继承ThreadPoolExecutor类可以使用其protected方法,如Hock method进行日志等扩展
详情参考 jdk-8u221-docs-all/docs/api/index.html
Factory method
但是呢,大多数现实需求的线程池场景,jdk已经帮你设定好了,你只需要从Executors类选择工厂方法即可创建,挑选你需要的♂
3种常用线程池
在Executors类种实现
newFixedThreadPool
固定线程数的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
//corePoolSize == maximumPoolSize == nThreads
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
newCachedThreadPool
弹性线程池,对于新任务,如果此时线程池没有空闲线程,线程池会立即新建线程。
当然是在不超过最大线程数Integer.MAX_VALUE的前提下。
//可传入ThreadFactory threadFactory参数
//功能是,自定义创建新线程时使用的线程工厂。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
SingleThreadExecutor
单个线程的线程池Executor
注:这个池子,就是上面我提及到的线程顺序执行的【线程池控制方法】
//保证任务按顺序执行,并且不会有多个任务处于活动状态 在任何给定的时间。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
b 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
注1:如果此单个线程由于在关闭之前执行期间的故障而终止,则在需要执行后续任务时将使用新的线程。
注2:与其他等效的newFixedThreadPool(1);
不同,SingleThreadExecutor保证返回的executor不可被重新配置以使用additional(其他的)线程。
自定义线程池(构造函数)
构造方法可以让我们自定义(扩展)线程池
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
corePoolSize:指定核心线程数量
maximumPoolSize:指定最大线程数量
keepAliveTime:当线程数大于核心时,多余空闲线程在终止之前等待新任务的最长时间
unit:为keepAliveTime计时创建的时间对象
workQueue:用于在任务执行前保存任务的队列。只包含execute方法提交的Runnable.run
threadFactory:创建新线程时使用的线程工厂
handler:任务拒绝策略
参数提要
corePoolSize&maximumPoolSize 线程数量:
- 如果运行线程的数量<核心线程数量,则创建新的线程处理请求
- 如果运行线程的数量>核心线程数量,<最大线程数量,则当workqueue满时创建新线程
- 如果核心线程数量==最大线程数量,那么将创建固定大小的连接池
- 如果设置了最大线程数量为无穷,那么允许线程池适合任意数量的并发请求
线程空闲时间:
- 当前线程数大于核心线程数情况下,如果超过空闲时间了,多余空闲的线程会被销毁
workqueue队列策略:
- 同步移交:不会放到队列中,而是等待线程执行它。如果当前线程没有执行,很可能会新开一个线程执行。当然也有可能被拒绝任务。
- 无界限队列策略:如果运行线程==核心线程数量都在CPU并发执行,则新进任务会放到队列中等待。运行线程数永远不会超过核心线程数。但存储队列会消耗内存
- 有界限队列策略:有限队列ArrayBlockingQueue与有限maximumPoolSizes一起使用时有助于防止系统资源(内存)耗尽。一般是两个模式,①小线程池大队列,有助于最小化CPU使用率、OS资源、上下文切换开销,但可能导致人为的低吞吐量。②大线程池小队列 ,有助于疏解多线程阻塞问题(一般遭遇到的是I/O阻塞)这会使CPU更加繁忙,但可能会遇到不可接受的调度开销,这也会降低吞吐量。
线程关闭或者线程数量达到MAX和阻塞队列饱和,就有拒绝任务的情况出现。
拒绝任务策略:
- 直接抛出异常
- 使用调用者的线程来处理
- 直接丢掉这个任务
- 丢掉最老的任务
实现Executor.execute()
public void execute(Runnable command) {
//如果传入的Runnable.run()任务是空的,则抛出空指针异常
if (command == null)
throw new NullPointerException();
//前面我省略说了ctl属性,ctl记录了线程池中任务数量和线程池状态(二进制的方法)
int c = ctl.get();
//Step1:如果处于运行状态的线程数小于核心线程数,则直接新建一个进程,不论有无空闲线程
if (workerCountOf(c) < corePoolSize) {
//这里通过返回false可防止在失败添加线程时抛出错误警报。
if (addWorker(command, true))
return;
c = ctl.get();
}
//Step2:如果任务成功排入队列WorkQueue,我们仍需要检查线程池是否应该添加一个线程
//(因为自上次检查后现有的线程可能已经死亡),或者自这个任务进入队列后线程池关闭
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//如果线程池不是RUNNING状态,且成功从阻塞队列中删除任务
if (! isRunning(recheck) && remove(command))
//则该任务由当前 RejectedExecutionHandler 处理。
reject(command);
//Step3:如果线程池正在运行,但无法令任务进入阻塞队列(队列已满),则新建线程
else if (workerCountOf(recheck) == 0)
//注意:新建线程对应的任务为null
addWorker(null, false);
}
//如果以上步骤都不行【可能的原因:线程池关闭、队列&最大线程数达到上限】
else if (!addWorker(command, false))
//则将任务由当前 RejectedExecutionHandler 处理。
reject(command);
}
线程池关闭
ThreadPoolExecutor提供了shutdown()
和shutdownNow()
两个方法来关闭线程池
浅显比喻:
一个是台式机正常关机(退出所有程序)拔电,一个是台式机直接拔电。
实际区别:
- 调用shutdown(),线程池状态立刻变为SHUTDOWN,调用shutdownNow(),线程池状态立刻变为STOP。
- shutdown()等待当前属于运行状态的线程(包含任务)执行完才中断线程,而shutdownNow()不等任务执行完就中断线程。
shutdown源码
启动有序关闭,先前提交的任务与正在执行的任务都将完整执行,但不接受任何新任务
注:如果不想执行workqueue队列中等待的任务,只执行运行状态的线程(包含任务),可执行 public boolean awaitTermination(long timeout, TimeUnit unit)
public void shutdown() {
//获取锁
final ReentrantLock mainLock = this.mainLock;
//锁定等待线程状态,禁止线程池其他线程进入执行状态
//Lock held on access to workers set and related bookkeeping(?).
mainLock.lock();
try {
//检测是否对工作线程拥有中断权限
checkShutdownAccess();
//设置线程池为shutdown状态
advanceRunState(SHUTDOWN);
//中断空闲线程
interruptIdleWorkers();
//在本类中此函数no-op,但在STPExecutor中用于取消延迟的任务
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
//如果(SHUTDOWN,池和队列为空)或(STOP和池为空),则转换为TERMINATED状态。
tryTerminate();
}
shutdownNow源码
尝试停止所有正在执行的任务(线程),停止处理等待的任务(workqueue),并返回等待执行的任务列表。从该方法返回时,将从任务队列中删除这些任务。
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
//锁定等待线程状态,禁止线程池其他线程进入执行状态
mainLock.lock();
try {
//检测是否对工作线程拥有中断权限
checkShutdownAccess();
//设置线程池为stop状态
advanceRunState(STOP);
//中断全部线程!!!重点,shutdown是interruptIdleWorkers,now没Id
interruptWorkers();
//返回drainqueue队列<List>内全部成员,同时清空drainqueue队列
tasks = drainQueue();
} finally {
mainLock.unlock();
}
//如果(SHUTDOWN,池和队列为空)或(STOP和池为空),则转换为TERMINATED状态。
tryTerminate();
return tasks;
}
详述看下面的大佬
关于锁
emmm不写锁,确实多线程不完整。多线程1/3是玩弄线程,1/3是玩弄各种锁。
多线程到此为止就算是入门了。之后单独开一篇锁入门。