并发编程(一)多线程基础和原理
多线程基础
最近,准备回顾下多线程相关的知识体系,顺便在这里做个记录。
并发的发展历史
最早的计算机只能解决简单的数学运算问题,比如正弦、 余弦等。运行方式:程序员首先把程序写到纸上,然后穿 孔成卡片,再把卡片盒带入到专门的输入室。输入室会有 专门的操作员将卡片的程序输入到计算机上。计算机运行 完当前的任务以后,把计算结果从打印机上进行输出,操 作员再把打印出来的结果送入到输出室,程序员就可以从 输出室取到结果。然后,操作员再继续从已经送入到输入 室的卡片盒中读入另一个任务重复上述的步骤。
操作员在机房里面来回调度资源, 以及计算机同一个时刻 只能运行一个程序, 在程序输入的过程中,计算机计算机 和处理空闲状态 。而当时的计算机是非常昂贵的,人们为了减少这种资源的浪费。就采用了 批处理系统来解决
并发的发展历史
批处理操作系统的运行方式:在输入室收集全部的作业, 然后用一台比较便宜的计算机把它们读取到磁带上。然后 把磁带输入到计算机,计算机通过读取磁带的指令来进行 运算,最后把结果输出磁带上。批处理操作系统的好处在 于,计算机会一直处于运算状态,合理的利用了计算机资 源。(运行流程如下图所示)
a: 程序员把卡片拿到1401机
b: 1401机把批处理作业读到磁带上
c: 操作员把输入磁带送到熬7094机
d: 7094机进行计算
e: 操作员把输出磁带送到1401机
f:1401机打印输出
批处理操作系统虽然能够解决计算机的空闲问题,但是当某一个作业因为等待磁盘或者其他 I/O 操作而暂停时 ,那 CPU 就只能阻塞直到该 I/O 完成,对于 CPU 操作密集型 的程序,I/O 操作相对较少,因此浪费的时间也很少。但是 对于 I/O 操作较多的场景来说, CPU 的资源是属于严重浪 费的。
集成电路和多道程序设计
多道程序设计的出现解决了这个问题,就是把内存分为几 个部分,每一个部分放不同的程序。当一个程序需要等待 I/O操作完成时。那么CPU可以切换执行内存中的另外一 个程序。如果内存中可以同时存放足够多的程序,那CPU 的利用率可以接近100%。 在这个时候,引入了第一个概念- 进程, 进程的本质是一个 正在执行的程序,程序运行时系统会创建一个进程,并且 给每个进程分配独立的内存地址空间保证每个进程地址不 会相互干扰。同时,在CPU对进程做时间片的切换时,保 证进程切换过程中仍然要从进程切换之前运行的位置出开 始执行。所以进程通常还会包括程序计数器、堆栈指针。
有了进程以后,可以让操作系统从宏观层面实现多应用并 发。而并发的实现是通过 CPU 时间片不端切换执行的。对 于单核 CPU 来说,在任意一个时刻只会有一个进程在被 CPU 调度
线程的出现
有了进程以后,为什么还会发明线程呢?
- 在多核CPU中,利用多线程可以实现真正意义上的并行 执行
- 在一个应用进程中,会存在多个同时执行的任务,如果 其中一个任务被阻塞,将会引起不依赖该任务的任务也 被阻塞。通过对不同任务创建不同的线程去处理,可以 提升程序处理的实时性
- 线程可以认为是轻量级的进程,所以线程的创建、销毁 比进程更快
线程的应用
如何应用多线程
在 Java 中,有多种方式来实现多线程。继承 Thread 类、 实现 Runnable 接口、使用 ExecutorService、Callable、 Future实现带返回结果的多线程。
继承 Thread 类创建线程
Thread类本质上是实现了Runnable接口的一个实例,代 表一个线程的实例。启动线程的唯一方法就是通过Thread 类的start()实例方法。start()方法是一个native方法,它会 启动一个新线程,并执行run()方法。这种方式实现多线程 很简单,通过自己的类直接extend Thread,并复写run() 方法,就可以启动新线程并执行自己定义的run()方法。
public class MyThread extends Thread {
public void run() { System.out.println("MyThread.run()"); }
} MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
实现 Runnable 接口创建线程
如果自己的类已经extends另一个类,就无法直接extends Thread,此时,可以实现一个Runnable接口
public class MyThread extends OtherClass implements Runnable {
public void run() {
System.out.println("MyThread.run()"); }
}
实现Callable接口通过FutureTask 包装器来创建 Thread 线程
public class CallableDemo implements Callable<String> {public static void main(String[] args) throws ExecutionException, InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(1);CallableDemo callableDemo = new CallableDemo();Future<String> future = executorService.submit(callableDemo);System.out.println(future.get());executorService.shutdown();}@Overridepublic String call() throws Exception {int a = 1;int b = 2;System.out.println(a + b);return "执行结果:" + (a + b);}
}
多线程的实际应用场景
其实大家在工作中应该很少有场景能够应用多线程了,因 为基于业务开发来说,很多使用异步的场景我们都通过分布式消息队列来做了。但并不是说多线程就不会被用到, 你们如果有看一些框架的源码,会发现线程的使用无处不在。
我在业务当中遇到比较多的,一类是大量数据的查询汇总,比如要统计一年的订单数据,这类大量数据的查询通常比较耗时,按时间段用多个线程查询数据,最后汇总处理,可以大大缩短系统的响应时间。
总结来说就是在进行比较耗时的IO,网络资源这类操作时,我们可以利用多核CPU的优势,合理的分配线程资源,从而达到任务的负载,提高系统性能。
下面是zookeeper源码中一个比较有意思的异步责任链模式
public class Request {private String name;public String getName() {return name;}public void setName(String name) {this.name = name;}@Overridepublic String toString() {return "Request{" +"name='" + name + '\'' +'}';}
}
public interface IRequestProcessor {void process(Request request);}
public class PrintProcessor extends Thread implements IRequestProcessor{//阻塞队列LinkedBlockingQueue<Request> requests=new LinkedBlockingQueue<>();private IRequestProcessor nextProcessor;private volatile boolean isFinish=false;public PrintProcessor() {}public PrintProcessor(IRequestProcessor nextProcessor) {this.nextProcessor = nextProcessor;}public void shutdown(){ //对外提供关闭的方法isFinish=true;}@Overridepublic void run() {while(!isFinish){ //不建议这么写try {Request request=requests.take();//阻塞式获取数据 //消费者//真正的处理逻辑System.out.println("PrintProcessor:"+request);//交给下一个责任链if(nextProcessor!=null) {nextProcessor.process(request);}} catch (InterruptedException e) {e.printStackTrace();}}}@Overridepublic void process(Request request) {//TODO 根据实际需求去做一些处理requests.add(request); //生产者}
}
public class SaveProcessor extends Thread implements IRequestProcessor{//阻塞队列LinkedBlockingQueue<Request> requests=new LinkedBlockingQueue<>();private IRequestProcessor nextProcessor;private volatile boolean isFinish=false;public SaveProcessor() {}public SaveProcessor(IRequestProcessor nextProcessor) {this.nextProcessor = nextProcessor;}public void shutdown(){ //对外提供关闭的方法isFinish=true;}@Overridepublic void run() {while(!isFinish){ //不建议这么写try {Request request=requests.take();//阻塞式获取数据//真正的处理逻辑; store to mysql 。System.out.println("SaveProcessor:"+request);//交给下一个责任链nextProcessor.process(request);} catch (InterruptedException e) {e.printStackTrace();}}}@Overridepublic void process(Request request) {//TODO 根据实际需求去做一些处理requests.add(request);}
}
public class App{static IRequestProcessor requestProcessor;public void setUp(){PrintProcessor printProcessor=new PrintProcessor();printProcessor.start();SaveProcessor saveProcessor=new SaveProcessor(printProcessor);saveProcessor.start();requestProcessor=new PrevProcessor(saveProcessor);((PrevProcessor) requestProcessor).start();}public static void main(String[] args) {App app=new App();app.setUp();Request request=new Request();request.setName("Mic");requestProcessor.process(request);}}
输出:
prevProcessor:Request{name='Mic'}
SaveProcessor:Request{name='Mic'}
PrintProcessor:Request{name='Mic'}
Java 并发编程的基础
基本应用搞清楚以后,我们再来基于Java线程的基础切入, 来逐步去深入挖掘线程的整体模型。
线程的生命周期
Java 线程既然能够创建,那么也势必会被销毁,所以线程 是存在生命周期的,那么我们接下来从线程的生命周期开 始去了解线程。
线程一共有 6 种状态(NEW、RUNNABLE、BLOCKED、 WAITING、TIME_WAITING、TERMINATED)
NEW:初始状态,线程被构建,但是还没有调用start方法
RUNNABLED:运行状态,JAVA线程把操作系统中的就绪 和运行两种状态统一称为“运行中”
BLOCKED:阻塞状态,表示线程进入等待状态,也就是线程 因为某种原因放弃了CPU使用权,阻塞也分为几种情况
➢ 等待阻塞:运行的线程执行 wait 方法,jvm 会把当前 线程放入到等待队列
➢ 同步阻塞:运行的线程在获取对象的同步锁时,若该同 步锁被其他线程锁占用了,那么 jvm 会把当前的线程 放入到锁池中
➢ 其他阻塞:运行的线程执行Thread.sleep或者t.join方法,或者发出了 I/O 请求时,JVM 会把当前线程设置为阻塞状态,当sleep结束、join线程终止、io处理完毕则线程恢复
WAITING:等待状态
TIME_WAITING:超时等待状态,超时以后自动返回
TERMINATED:终止状态,表示当前线程执行完毕
通过代码演示线程的状态
public class ThreadStatusDemo {public static void main(String[] args) {new Thread(()->{while(true){try {TimeUnit.SECONDS.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}},"Time_Waiting_Thread").start();new Thread(()->{while(true){synchronized (ThreadStatusDemo.class) {try {ThreadStatusDemo.class.wait();} catch (InterruptedException e) {e.printStackTrace();}}}},"Wating_Thread").start();//BLOCKEDnew Thread(new BlockedDemo(),"Blocke01_Thread").start();new Thread(new BlockedDemo(),"Blocke02_Thread").start();}static class BlockedDemo extends Thread{@Overridepublic void run() {synchronized (BlockedDemo.class){while(true){try {TimeUnit.SECONDS.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}}}
}
启动一个线程前,最好为这个线程设置线程名称,因为这 样在使用jstack分析程序或者进行问题排查时,就会给开 发人员提供一些提示
显示线程的状态
➢ 运行该示例,打开终端或者命令提示符,键入“jps”, (JDK1.5 提供的一个显示当前所有 java 进程 pid 的命 令)
➢ 根据上一步骤获得的pid,继续输入jstack pid(jstack 是 java 虚拟机自带的一种堆栈跟踪工具。jstack 用于 打印出给定的 java 进程 ID 或 core file 或远程调试服 务的Java堆栈信息)
通过上面的分析,我们了解到了线程的生命周期,现在在 整个生命周期中并不是固定的处于某个状态,而是随着代 码的执行在不同的状态之间进行切换
线程的启动
前面我们通过一些案例演示了线程的启动,也就是调用 start()方法去启动一个线程,当run方法中的代码执行完毕 以后,线程的生命周期也将终止。调用start方法的语义是 当前线程告诉JVM,启动调用start方法的线程。
线程的启动原理
很多同学最早学习线程的时候会比较疑惑,启动一个线程 为什么是调用start方法,而不是run方法,这做一个简单 的分析,先简单看一下start方法的定义
我们看到调用 start 方法实际上是调用一个 native 方法 start0()来启动一个线程,首先 start0()这个方法是在 Thread的静态块中来注册的,代码如下
registerNatives 的 本 地 方 法 的 定 义 在 文 件 Thread.c,Thread.c定义了各个操作系统平台要用的关于线 程的公共数据和操作,以下是Thread.c的全部内容
http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/00cd9dc3c2b5/src/share/native/java/lang/Thread.c
从 这 段 代 码 可 以 看 出 , start0() , 实 际 会 执 行 JVM_StartThread方法,这个方法是干嘛的呢? 从名字上 来看,似乎是在JVM层面去启动一个线程,如果真的是这 样,那么在 JVM 层面,一定会调用 Java 中定义的 run 方法。那接下来继续去找找答案。我们找到 jvm.cpp这个文 件;这个文件需要下载hotspot的源码才能找到.
JVM_ENTRY 是用来定义 JVM_StartThread 函数的,在这 个函数里面创建了一个真正和平台有关的本地线程. 本着 打破砂锅查到底的原则,继续看看 newJavaThread做了什 么事情,继续寻找JavaThread的定义 在hotspot的源码中 thread.cpp文件中1558行的位置可 以找到如下代码
这个方法有两个参数,第一个是函数名称,线程创建成功 之后会根据这个函数名称调用对应的函数;第二个是当前 进程内已经有的线程数量。最后我们重点关注与一下 os::create_thread,实际就是调用平台创建线程的方法来创 建线程。 接下来就是线程的启动,会调用 Thread.cpp 文件中的 Thread::start(Thread* thread)方法,代码如下
start 方法中有一个函数调用: os::start_thread(thread);, 调用平台启动线程的方法,最终会调用Thread.cpp文件中 的JavaThread::run()方法
线程的中止
线程的启动过程大家都非常熟悉,但是如何终止一个线程 呢? 这是面试过程中喜欢问到的一个 题目。 线程的终止,并不是简单的调用stop命令去。虽然api仍 然可以调用,但是和其他的线程控制方法如 suspend、 resume 一样都是过期了的不建议使用,就拿 stop 来说, stop方法在结束一个线程时并不会保证线程的资源正常释 放,因此会导致程序可能出现一些不确定的状态。 要优雅的去中断一个线程,在线程中提供了一个 interrupt 方法
interrupt 方法
当其他线程通过调用当前线程的interrupt方法,表示向当 前线程打个招呼,告诉他可以中断线程的执行了,至于什 么时候中断,取决于当前线程自己。 线程通过检查资深是否被中断来进行相应,可以通过 isInterrupted()来判断是否被中断。 通过下面这个例子,来实现了线程终止的逻辑
public class InterruptDemo {private static int i;public static void main(String[] args) throws InterruptedException {Thread thread=new Thread(()->{while(!Thread.currentThread().isInterrupted()){//默认是false _interrupted state?i++;}System.out.println("i:"+i);});thread.start();TimeUnit.SECONDS.sleep(1);thread.interrupt(); //把isInterrupted设置成true}
}
输出:i:343782411
这种通过标识位或者中断操作的方式能够使线程在终止时 有机会去清理资源,而不是武断地将线程停止,因此这种 终止线程的做法显得更加安全和优雅
Thread.interrupted
上面的案例中,通过 interrupt,设置了一个标识告诉线程 可 以 终 止 了 , 线 程 中 还 提 供 了 静 态 方 法 Thread.interrupted()对设置中断标识的线程复位。比如在 上面的案例中,外面的线程调用thread.interrupt来设置中 断标识,而在线程里面,又通过 Thread.interrupted 把线 程的标识又进行了复位
public class ThreadResetDemo {//1. Thread.interrupted()//2. InterruptedExceptionpublic static void main(String[] args) throws InterruptedException {Thread thread=new Thread(()->{while(true){//默认是false _interrupted state?if(Thread.currentThread().isInterrupted()){System.out.println("before:"+Thread.currentThread().isInterrupted());Thread.interrupted(); //复位- 回到初始状态System.out.println("after:"+Thread.currentThread().isInterrupted());}}});thread.start();TimeUnit.SECONDS.sleep(1);thread.interrupt(); //把isInterrupted设置成true}
}
为什么要复位
Thread.interrupted()是属于当前线程的,是当前线程对外 界中断信号的一个响应,表示自己已经得到了中断信号, 但不会立刻中断自己,具体什么时候中断由自己决定,让 外界知道在自身中断前,他的中断状态仍然是false,这就 是复位的原因。
线程的终止原理
我们来看一下 thread.interrupt()方法做了什么事情
这个方法里面,调用了interrupt0(),这个方法在前面分析 start方法的时候见过,是一个native方法,这里就不再重 复贴代码了,同样,我们找到 jvm.cpp 文件,找到 JVM_Interrupt的定义
这个方法比较简单,直接调用了 Thread::interrupt(thr)这 个方法,这个方法的定义在Thread.cpp文件中,代码如下
Thread::interrupt方法调用了os::interrupt方法,这个是调 用平台的 interrupt 方法,这个方法的实现是在 os_*.cpp 文件中,其中星号代表的是不同平台,因为 jvm 是跨平台 的,所以对于不同的操作平台,线程的调度方式都是不一 样的。我们以os_linux.cpp文件为例
set_interrupted(true)实际上就是调用 osThread.hpp 中的 set_interrupted()方法,在 osThread 中定义了一个成员属 性volatile jint _interrupted;
通过上面的代码分析可以知道,thread.interrupt()方法实际 就是设置一个 interrupted 状态标识为 true、并且通过 ParkEvent的unpark方法来唤醒线程。
- 对于synchronized阻塞的线程,被唤醒以后会继续尝试 获取锁,如果失败仍然可能被park
- 在调用ParkEvent的park方法之前,会先判断线程的中 断状态,如果为true,会清除当前线程的中断标识
- Object.wait 、 Thread.sleep 、 Thread.join 会 抛 出 InterruptedException
这里给大家普及一个知识点,为什么 Object.wait 、 Thread.sleep 和 Thread.join 都 会 抛 出 InterruptedException? 你会发现这几个方法有一个共同 点,都 是属于阻塞的方法 而阻塞方法的释放会取决于一些外部的事件 , 但是阻塞方 法可能因为等不到外部的触发事件而导致无法终止,所以 它允许一个线程请求 自己 来停止它正在做的事情。当一个 方法抛出 InterruptedException 时,它是在告诉调用者如 果执行该方法的线程被中断,它会尝试停止正在做的事情 并且通过抛出 InterruptedException 表示提前返回。
所以 ,这 个 异 常 的 意 思 是 表 示 一 个 阻 塞 被 其 他 线 程 中 断 了 。 然后,由于线程调用了 interrupt() 中断方法,那么
Object.wait 、 Thread.sleep 等被阻塞的线程被唤醒以后会 通过 is_interrupted 方 法 判 断 中 断 标 识 的 状 态 变 化 ,如 果 发 现中断标识为 true ,则先清除中断标识,然后抛出 InterruptedException
需要注意的是,InterruptedException异常的抛出并不意味 着线程必须终止,而是提醒当前线程有中断的操作发生, 至于接下来怎么处理取决于线程本身,比如 1. 直接捕获异常不做任何处理 2. 将异常往外抛出 3. 停止当前线程,并打印异常信息 为了让大家能够更好的理解上面这段话,我们以 Thread.sleep 为例直接从 jdk 的源码中找到中断标识的清 除以及异常抛出的方法代码 找到 is_interrupted()方法,linux 平台中的实现在 os_linux.cpp文件中,代码如下
找到Thread.sleep这个操作在jdk中的源码体现,怎么找?代码 在jvm.cpp文件中
注意上面加了中文注释的地方的代码,先判断 is_interrupted 的 状 态 , 然 后 抛 出 一 个 InterruptedException异常。到此为止,我们就已经分析清 楚了中断的整个流程。
并发编程(一)多线程基础和原理相关推荐
- 安琪拉教百里守约学并发编程之多线程基础
<安琪拉与面试官二三事>系列文章 一个HashMap能跟面试官扯上半个小时 一个synchronized跟面试官扯了半个小时 <安琪拉教鲁班学算法>系列文章 安琪拉教鲁班学算法 ...
- 并发编程之多线程基础-守护线程与非守护线程(四)
守护线程概念: 只要当前JVM实例中尚存在任何一个非守护线程没有结束, 守护线程就全部工作; 只有当最后一个非守护线程结 束时, 守护线程随着 JVM 一同结束工作. 守护线程最典型的应用就是 GC ...
- 并发编程之多线程基础-Thread和Runnable的区别及联系(二)
上篇文章讲述了创建线程的常用方式 本篇主要分析一下Thread和Runnable两种方式创建线程的区别及联系 联系: ▶Thread类实现了Runable接口. ▶都需要重写里面Run方法. 区别: ...
- java内存栅栏_内存屏障(Memory Barriers/Fences) - 并发编程中最基础的一项技术
我们经常都听到并发编程,但很多人都被其高大上的感觉迷惑而停留在知道听说这一层面,下面我们就来讨论并发编程中最基础的一项技术:内存屏障或内存栅栏,也就是让一个CPU处理单元中的内存状态对其它处理单元可见 ...
- 并发编程系列之AQS实现原理
并发编程系列之AQS实现原理 1.什么是AQS? AQS(AbstractQueuedSynchronizer),抽象队列同步器,是juc中很多Lock锁和同步组件的基础,比如CountDownLat ...
- 内存屏障(Memory Barriers/Fences) - 并发编程中最基础的一项技术
内存屏障(Memory Barriers/Fences) - 并发编程中最基础的一项技术_chuhan0449的博客-CSDN博客 我们经常都听到并发编程,但很多人都被其高大上的感觉迷惑而停留在知道听 ...
- c+++11并发编程语言,C++11并发编程:多线程std:thread
原标题:C++11并发编程:多线程std:thread 一:概述 C++11引入了thread类,大大降低了多线程使用的复杂度,原先使用多线程只能用系统的API,无法解决跨平台问题,一套代码平台移植, ...
- week6 day4 并发编程之多线程 理论
week6 day4 并发编程之多线程 理论 一.什么是线程 二.线程的创建开销小 三.线程和进程的区别 四.为何要用多线程 五.多线程的应用举例 六.经典的线程模型(了解) 七.POSIX线程(了解 ...
- 【Java 并发编程】多线程、线程同步、死锁、线程间通信(生产者消费者模型)、可重入锁、线程池
并发编程(Concurrent Programming) 进程(Process).线程(Thread).线程的串行 多线程 多线程的原理 多线程的优缺点 Java并发编程 默认线程 开启新线程 `Ru ...
最新文章
- 未来的电子计算机作文500字,我的新计算机作文500字
- 禁止UDP端口引起DNS错误导致邮局无法外发的故障
- 一天学完spark的Scala基础语法教程七、数组(idea版本)
- LeetCode之Remove Element
- 浪漫情人节|C语言画心型
- 计算机裸机与应用程序及用户之间的桥梁是,2016计算机二级《MS Office》单选试题与解析...
- 【笔记】汇编..寄存器和地址的概述
- ProviderManager
- centos7如何安装samba-client_Docker: 教程07 - ( 如何对 Docker 进行降级和升级)
- java中事物的注解_JAVA中对事物的理解
- JavaScript 在线编辑器
- 12306网站车票爬取
- 水溶性羧基化 CdSe/ZnS 量子的特点
- 亚马逊账号被关联能申诉得回来吗
- 微信会不会封服务器ip,最新微信防封号设置技巧(新微信如何防止封号)
- 适合户外运动的蓝牙耳机品牌有哪些呀?户外运动耳机排行榜
- 多域名SSL证书是什么意思?
- 知道挖掘搜索引擎关键字的步骤吗?
- springboot毕设项目公共场所安保信息管理系统v2rtn(java+VUE+Mybatis+Maven+Mysql)
- 易宝支付 下单失败! 失败原因: 业务接口维护中,请您稍候再试!
热门文章
- 云原生改造的实现路径
- LaTex排版 正文间距(段行列间距)调整与表格调整(宽度, 合并, 表注)
- oracle在Windows,linux备份恢复(tina)
- android usb联接网络打印机,打印到USB或预先选择的网络打印机从嵌入式android
- flowable6.4 并行网关 驳回 跳转 回退 多实例加签减签
- Windows Server 2008上安装Media Player
- transformer股票步骤
- 奉主耶稣基督的名,斥责一切魔鬼撒旦黑暗势力对我的捆绑,斥责一切邪灵对我的束缚,仇敌必然逃跑
- 关于各种USB启动模式(MBR)的原理剖析
- 快来,票字版软件电子发票的设置方式(详细流程)