多线程基础

 最近,准备回顾下多线程相关的知识体系,顺便在这里做个记录。

并发的发展历史

最早的计算机只能解决简单的数学运算问题,比如正弦、 余弦等。运行方式:程序员首先把程序写到纸上,然后穿 孔成卡片,再把卡片盒带入到专门的输入室。输入室会有 专门的操作员将卡片的程序输入到计算机上。计算机运行 完当前的任务以后,把计算结果从打印机上进行输出,操 作员再把打印出来的结果送入到输出室,程序员就可以从 输出室取到结果。然后,操作员再继续从已经送入到输入 室的卡片盒中读入另一个任务重复上述的步骤。

操作员在机房里面来回调度资源, 以及计算机同一个时刻 只能运行一个程序, 在程序输入的过程中,计算机计算机 和处理空闲状态 。而当时的计算机是非常昂贵的,人们为了减少这种资源的浪费。就采用了 批处理系统来解决

并发的发展历史

批处理操作系统的运行方式:在输入室收集全部的作业, 然后用一台比较便宜的计算机把它们读取到磁带上。然后 把磁带输入到计算机,计算机通过读取磁带的指令来进行 运算,最后把结果输出磁带上。批处理操作系统的好处在 于,计算机会一直处于运算状态,合理的利用了计算机资 源。(运行流程如下图所示)


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 调度

线程的出现

有了进程以后,为什么还会发明线程呢?

  1. 在多核CPU中,利用多线程可以实现真正意义上的并行 执行
  2. 在一个应用进程中,会存在多个同时执行的任务,如果 其中一个任务被阻塞,将会引起不依赖该任务的任务也 被阻塞。通过对不同任务创建不同的线程去处理,可以 提升程序处理的实时性
  3. 线程可以认为是轻量级的进程,所以线程的创建、销毁 比进程更快

线程的应用

如何应用多线程
在 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方法来唤醒线程。

  1. 对于synchronized阻塞的线程,被唤醒以后会继续尝试 获取锁,如果失败仍然可能被park
  2. 在调用ParkEvent的park方法之前,会先判断线程的中 断状态,如果为true,会清除当前线程的中断标识
  3. 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异常。到此为止,我们就已经分析清 楚了中断的整个流程。

并发编程(一)多线程基础和原理相关推荐

  1. 安琪拉教百里守约学并发编程之多线程基础

    <安琪拉与面试官二三事>系列文章 一个HashMap能跟面试官扯上半个小时 一个synchronized跟面试官扯了半个小时 <安琪拉教鲁班学算法>系列文章 安琪拉教鲁班学算法 ...

  2. 并发编程之多线程基础-守护线程与非守护线程(四)

    守护线程概念: 只要当前JVM实例中尚存在任何一个非守护线程没有结束, 守护线程就全部工作; 只有当最后一个非守护线程结 束时, 守护线程随着 JVM 一同结束工作. 守护线程最典型的应用就是 GC ...

  3. 并发编程之多线程基础-Thread和Runnable的区别及联系(二)

    上篇文章讲述了创建线程的常用方式 本篇主要分析一下Thread和Runnable两种方式创建线程的区别及联系 联系: ▶Thread类实现了Runable接口. ▶都需要重写里面Run方法. 区别: ...

  4. java内存栅栏_内存屏障(Memory Barriers/Fences) - 并发编程中最基础的一项技术

    我们经常都听到并发编程,但很多人都被其高大上的感觉迷惑而停留在知道听说这一层面,下面我们就来讨论并发编程中最基础的一项技术:内存屏障或内存栅栏,也就是让一个CPU处理单元中的内存状态对其它处理单元可见 ...

  5. 并发编程系列之AQS实现原理

    并发编程系列之AQS实现原理 1.什么是AQS? AQS(AbstractQueuedSynchronizer),抽象队列同步器,是juc中很多Lock锁和同步组件的基础,比如CountDownLat ...

  6. 内存屏障(Memory Barriers/Fences) - 并发编程中最基础的一项技术

    内存屏障(Memory Barriers/Fences) - 并发编程中最基础的一项技术_chuhan0449的博客-CSDN博客 我们经常都听到并发编程,但很多人都被其高大上的感觉迷惑而停留在知道听 ...

  7. c+++11并发编程语言,C++11并发编程:多线程std:thread

    原标题:C++11并发编程:多线程std:thread 一:概述 C++11引入了thread类,大大降低了多线程使用的复杂度,原先使用多线程只能用系统的API,无法解决跨平台问题,一套代码平台移植, ...

  8. week6 day4 并发编程之多线程 理论

    week6 day4 并发编程之多线程 理论 一.什么是线程 二.线程的创建开销小 三.线程和进程的区别 四.为何要用多线程 五.多线程的应用举例 六.经典的线程模型(了解) 七.POSIX线程(了解 ...

  9. 【Java 并发编程】多线程、线程同步、死锁、线程间通信(生产者消费者模型)、可重入锁、线程池

    并发编程(Concurrent Programming) 进程(Process).线程(Thread).线程的串行 多线程 多线程的原理 多线程的优缺点 Java并发编程 默认线程 开启新线程 `Ru ...

最新文章

  1. 未来的电子计算机作文500字,我的新计算机作文500字
  2. 禁止UDP端口引起DNS错误导致邮局无法外发的故障
  3. 一天学完spark的Scala基础语法教程七、数组(idea版本)
  4. LeetCode之Remove Element
  5. 浪漫情人节|C语言画心型
  6. 计算机裸机与应用程序及用户之间的桥梁是,2016计算机二级《MS Office》单选试题与解析...
  7. 【笔记】汇编..寄存器和地址的概述
  8. ProviderManager
  9. centos7如何安装samba-client_Docker: 教程07 - ( 如何对 Docker 进行降级和升级)
  10. java中事物的注解_JAVA中对事物的理解
  11. JavaScript 在线编辑器
  12. 12306网站车票爬取
  13. 水溶性羧基化 CdSe/ZnS 量子的特点
  14. 亚马逊账号被关联能申诉得回来吗
  15. 微信会不会封服务器ip,最新微信防封号设置技巧(新微信如何防止封号)
  16. 适合户外运动的蓝牙耳机品牌有哪些呀?户外运动耳机排行榜
  17. 多域名SSL证书是什么意思?
  18. 知道挖掘搜索引擎关键字的步骤吗?
  19. springboot毕设项目公共场所安保信息管理系统v2rtn(java+VUE+Mybatis+Maven+Mysql)
  20. 易宝支付 下单失败! 失败原因: 业务接口维护中,请您稍候再试!

热门文章

  1. 云原生改造的实现路径
  2. LaTex排版 正文间距(段行列间距)调整与表格调整(宽度, 合并, 表注)
  3. oracle在Windows,linux备份恢复(tina)
  4. android usb联接网络打印机,打印到USB或预先选择的网络打印机从嵌入式android
  5. flowable6.4 并行网关 驳回 跳转 回退 多实例加签减签
  6. Windows Server 2008上安装Media Player
  7. transformer股票步骤
  8. 奉主耶稣基督的名,斥责一切魔鬼撒旦黑暗势力对我的捆绑,斥责一切邪灵对我的束缚,仇敌必然逃跑
  9. 关于各种USB启动模式(MBR)的原理剖析
  10. 快来,票字版软件电子发票的设置方式(详细流程)