多线程编程

1.线程概念

多线程编程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。

一条线程指的是进程中一个单一顺序的控制流一个进程中可以有多个线程,而每条线程可以执行不同的任务。

一个进程包括由操作系统分配的内存空间,包含一个或多个线程一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。

进程:是指计算机中已运行的程序(也包括由操作系统分配的内存空间),它是一个动态执行的过程。假设我们电脑上同时运行了浏览器、QQ 以及代码编辑器三个软件,这三个软件之所以同时运行,就是进程所起的作用。

线程:是操作系统能够进行运算调度的最小单位(是进程中一个单一顺序的控制流)。大部分情况下,它被包含在进程之中,是进程中的实际运作单位。也就是说一个进程可以包含多个线程, 因此线程也被称为轻量级进程。

对于进程和线程概念的搞懂文章:进程与线程的一个简单解释 - 阮一峰的网络日志

2.创建线程

在 Java 中,创建线程有以下 3 种方式:

  1. 继承 Thread 类,重写 run() 方法,该方法代表线程要执行的任务;
  2. 实现 Runnable 接口,实现 run() 方法,该方法代表线程要执行的任务;
  3. 实现 Callable 接口,实现 call() 方法,call() 方法作为线程的执行体,具有返回值,并且可以对异常进行声明和抛出。

2.1Thread 类

Thread 类是一个线程类,位于 java.lang 包下。

2.1.1 构造方法

Thread 类的常用构造方法如下:

  • Thread():创建一个线程对象;
  • Thread(String name):创建一个指定名称的线程对象;
  • Thread(Runnable target):创建一个基于 Runnable 接口实现类的线程对象;
  • Thread(Runnable target, String name):创建一个基于 Runnable 接口实现类,并具有指定名称的线程对象。

 2.1.2 常用方法

void run():线程相关的代码写在该方法中,一般需要重写;

void start():启动当前线程;

static void sleep(long m):使当前线程休眠 m 毫秒;

void join():优先执行调用 join() 方法的线程。

Tips:run() 方法是一个非常重要的方法,它是用于编写线程执行体的方法,不同线程之间的一个最主要区别就是 run() 方法中的代码是不同的。

可翻阅官方文档以查看更多 API。

2.1.3 实例

通过继承 Thread 类创建线程可分为以下 3 步:

  1. 定义 Thread 类的子类,并重写该类的 run() 方法。run() 方法的方法体就代表了线程要完成的任务;
  2. 创建 Thread 子类的实例,即创建线程对象;
  3. 调用线程对象的 start 方法来启动该线程。

具体实例如下:

/**
 * @author  庞鹏程
 */
public class ThreadDemo1 extends Thread {

    /**
     * 重写 Thread() 的方法
     */
    @Override
    public void run() {
        System.out.println("这里是线程体");
        // 当前打印线程的名称
        System.out.println(getName());
    }

    public static void main(String[] args) {
        // 实例化 ThreadDemo1 对象
        ThreadDemo1 threadDemo1 = new ThreadDemo1();
        // 调用 start() 方法,以启动线程
        threadDemo1.start();
    }

}

运行结果:

这里是线程体
Thread-0

小伙伴们可能会有疑问,上面这样的代码,和普通的类实例化以及方法调用有什么区别的,下面我们来看一个稍微复杂些的实例:

/**
 * @author 庞鹏程
 */
public class ThreadDemo2 {

    /**
     * 静态内部类
     */
    static class MyThread extends Thread {

        private int i = 3;

        MyThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            while (i > 0) {
                System.out.println(getName() + " i = " + i);
                i--;
            }
        }

    }

    public static void main(String[] args) {
        // 创建两个线程对象
        MyThread thread1 = new MyThread("线程1");
        MyThread thread2 = new MyThread("线程2");
        // 启动线程
        thread1.start();
        thread2.start();
    }

}

运行结果:

线程2 i = 3
线程1 i = 3
线程1 i = 2
线程2 i = 2
线程1 i = 1
线程2 i = 1

代码中我们是先启动了线程 1,再启动了线程 2 的,观察运行结果,线程并不是按照我们所预想的顺序执行的。这里就要划重点了,不同线程,执行顺序是随机的。如果你再执行几次代码,可以观察到每次的运行结果都可能不同

2.2 Runnable 接口

2.2.1 为什么需要 Runnable 接口

通过实现 Runnable 接口的方案来创建线程,要优于继承 Thread 类的方案,主要有以下原因:

  1. Java 不支持多继承,所有的类都只允许继承一个父类,但可以实现多个接口。如果继承了 Thread 类就无法继承其它类,这不利于扩展;
  2. 继承 Thread 类通常只重写 run() 方法,其他方法一般不会重写。继承整个 Thread 类成本过高,开销过大。

2.2.2 实例

通过实现 Runnable 接口创建线程的步骤如下:

  1. 定义 Runnable 接口的实现类,并实现该接口的 run() 方法。这个 run() 方法的方法体同样是该线程的线程执行体;
  2. 创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象;
  3. 调用线程对象的 start 方法来启动该线程。

具体实例如下:

/**
 * @author 庞鹏程
 */
public class RunnableDemo1 implements Runnable {

    private int i = 5;

    @Override
    public void run() {
        while (i > 0) {
            System.out.println(Thread.currentThread().getName() + " i = " + i);
            i--;
        }
    }

    public static void main(String[] args) {
        // 创建两个实现 Runnable 实现类的实例
        RunnableDemo1 runnableDemo1 = new RunnableDemo1();
        RunnableDemo1 runnableDemo2 = new RunnableDemo1();
        // 创建两个线程对象
        Thread thread1 = new Thread(runnableDemo1, "线程1");
        Thread thread2 = new Thread(runnableDemo2, "线程2");
        // 启动线程
        thread1.start();
        thread2.start();
    }

}

运行结果:

线程1 i = 5
线程1 i = 4
线程1 i = 3
线程1 i = 2
线程2 i = 5
线程1 i = 1
线程2 i = 4
线程2 i = 3
线程2 i = 2
线程2 i = 1

2.3 Callable 接口

2.3.1 为什么需要 Callable 接口

继承 Thread 类和实现 Runnable 接口这两种创建线程的方式都没有返回值。所以,线程执行完毕后,无法得到执行结果。为了解决这个问题,Java 5 后,提供了 Callable 接口和 Future 接口,通过它们,可以在线程执行结束后,返回执行结果。

2.3.2 实例

通过实现 Callable 接口创建线程步骤如下:

  1. 创建 Callable 接口的实现类,并实现 call() 方法。这个 call() 方法将作为线程执行体,并且有返回值;
  2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,这个 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值;
  3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程;
  4. 调用 FutureTask 对象的 get() 方法来获得线程执行结束后的返回值。

具体实例如下:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @author 庞鹏程
 */
public class CallableDemo1 {

    static class MyThread implements Callable {

        @Override
        public String call() { // 方法返回值类型是一个泛型,在上面 Callable 处定义
            return "我是线程中返回的字符串";
        }

    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 常见实现类的实例
        Callable callable = new MyThread();
        // 使用 FutureTask 类来包装 Callable 对象
        FutureTask futureTask = new FutureTask<>(callable);
        // 创建 Thread 对象
        Thread thread = new Thread(futureTask);
        // 启动线程
        thread.start();
        // 调用 FutureTask 对象的 get() 方法来获得线程执行结束后的返回值
        String s = futureTask.get();
        System.out.println(s);
    }

}

 运行结果:

我是线程中返回的字符串

3. 线程休眠

在前面介绍 Thread 类的常用方法时,我们介绍了 sleep() 静态方法,该方法可以使当前执行的线程睡眠(暂时停止执行)指定的毫秒数。

线程休眠的实例如下:

/**
 * @author 庞鹏程
 */
public class SleepDemo implements Runnable {

    @Override
    public void run() {
        for (int i = 1; i <= 5; i ++) {
            // 打印语句
            System.out.println(Thread.currentThread().getName() + ":执行第" + i + "次");
            try {
                // 使当前线程休眠
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    public static void main(String[] args) {
        // 实例化 Runnable 的实现类
        SleepDemo sleepDemo = new SleepDemo();
        // 实例化线程对象
        Thread thread = new Thread(sleepDemo);
        // 启动线程
        thread.start();
    }

}

运行结果为:休息一秒打印一次执行

Thread-0:执行第1次
Thread-0:执行第2次
Thread-0:执行第3次
Thread-0:执行第4次
Thread-0:执行第5次

4. 线程的状态和生命周期

java.lang.Thread.Starte 枚举类中定义了 6 种不同的线程状态:

  1. NEW:新建状态,尚未启动的线程处于此状态;
  2. RUNNABLE:可运行状态,Java 虚拟机中执行的线程处于此状态;
  3. BLOCK:阻塞状态,等待监视器锁定而被阻塞的线程处于此状态;
  4. WAITING:等待状态,无限期等待另一线程执行特定操作的线程处于此状态;
  5. TIME_WAITING:定时等待状态,在指定等待时间内等待另一线程执行操作的线程处于此状态;
  6. TERMINATED:结束状态,已退出的线程处于此状态。

值得注意的是,一个线程在给定的时间点只能处于一种状态。这些状态是不反映任何操作系统线程状态的虚拟机状态。

线程的生命周期,实际上就是上述 6 个线程状态的转换过程。下图展示了一个完整的生命周期:

多线程编程_第1张图片

5. 小结

通过本小节的学习,我们知道了线程是操作系统能够进行运算调度的最小单位。线程也被称为轻量级进程。在 Java 中,可以以 3 种方式创建线程,分别是继承 Thread 类、实现 Runnable 接口以及实现 Callable 接口。可以使用静态方法 sleep() 让线程休眠。线程状态有 6 种,也有资料上说线程有 5 种,这部分内容我们按照 Java 源码中的定义 6 种来记忆即可

创建线程的三种方式的对比

  • 1. 采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。

  • 2. 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。

线程的几个主要概念

在多线程编程时,你需要了解以下几个概念:

  • 线程同步
  • 线程间通信
  • 线程死锁
  • 线程控制:挂起、停止和恢复

多线程的使用

有效利用多线程的关键是理解程序是并发执行而不是串行执行的。例如:程序中有两个子系统需要并发执行,这时候就需要利用多线程编程。

通过对多线程的使用,可以编写出非常高效的程序。不过请注意,如果你创建太多的线程,程序执行的效率实际上是降低了,而不是提升了。

请记住,上下文的切换开销也很重要,如果你创建了太多的线程,CPU 花费在上下文的切换的时间将多于执行程序的时间!

并发和并行

  • 并发(concurrency):把任务在不同的时间点交给处理器进行处理。在同一时间点,任务并不会同时运行。
  • 并行(parallelism):把每一个任务分配给每一个处理器独立完成。在同一时间点,任务一定是同时运行。

一:

并发是指一个处理器同时处理多个任务。
并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。
并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。
来个比喻:并发是一个人同时吃三个馒头,而并行是三个人同时吃三个馒头。

二:

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。就好像两个人各拿一把铁锨在挖坑,一小时后,每人一个大坑。所以无论从微观还是从宏观来看,二者都是一起执行的。

并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。这就好像两个人用同一把铁锨,轮流挖坑,一小时后,两个人各挖一个小一点的坑,要想挖两个大一点得坑,一定会用两个小时。

并行在多处理器系统中存在,而并发可以在单处理器和多处理器系统中都存在,并发能够在单处理器系统中存在是因为并发是并行的假象,并行要求程序能够同时执行多个操作,而并发只是要求程序假装同时执行多个操作(每个小时间片执行一个操作,多个操作快速切换执行)。

三:
当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态.这种方式我们称之为并发(Concurrent)。

当系统有一个以上CPU时,则线程的操作有可能非并发.当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。

并发(concurrency)和并行(parallellism)是:

  1. 解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
  2. 解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件
  3. 解释三:并发在一台处理器上“同时”(宏观情况下)处理多个任务,并行在多台处理器上同时同一时刻处理多个任务

所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能

并行

并行(Parallel),当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。

这里面有一个很重要的点,那就是系统要有多个CPU才会出现并行。在有多个CPU的情况下,才会出现真正意义上的『同时进行』。

并发与并行生活案例

我们两个人在吃午饭。你在吃饭的整个过程中,吃了米饭、吃了蔬菜、吃了牛肉。吃米饭、吃蔬菜、吃牛肉这三件事其实就是并发执行的。对于你来说,整个过程中看似是同时完成的的。但其实你是在吃不同的东西之间来回切换的【并发】。

还是我们两个人吃午饭。在吃饭过程中,你吃了米饭、蔬菜、牛肉。我也吃了米饭、蔬菜和牛肉。

我们两个人之间的吃饭就是并行的。两个人之间可以在同一时间点一起吃牛肉,或者一个吃牛肉,一个吃蔬菜。之间是互不影响的【并行】。所以,并发是指在一段时间内宏观上多个程序同时运行。并行指的是同一个时刻,多个任务确实真的在同时运行。

并发和并行的区别

并发:指的是多个事情,在同一时间段内同时发生了。并行:指的是多个事情,在同一时间点上同时发生了。

并发的多个任务之间是互相抢占资源的。并行的多个任务之间是不互相抢占资源的,在多CPU的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。

多线程编程_第2张图片

同一个调用链路是不是同一个线程里

比如,我从controller调用service,然后dao,类似这样的一个调用链下来,其中中间是没有单独使用线程的话,这个链路是不是就是同一个线程操作?答:是对的

高并发是什么

高并发通常指的是通过设计来保证一个系统能够并行处理大量请求。

1.通常意义上讲,高并发是指许多用户同时访问相同 API接口或 URL地址。这种情况常常发生在活跃用户数量大、用户聚集程度高的业务场景中。

2.高并发(High Concurrency)是一种系统在运行时遇到的一种“短时间内遇到大量操作请求”的情况,主要发生在对 Web系统的大量访问中收到大量请求(例如:12306的抢票情况;天猫双十一活动)。这种情况的出现将导致系统在此时间内执行大量操作,如对资源的请求、数据库操作等。

3.高并发性相关的常用指标有响应时间(Response Time)、吞吐量(Throughput)、每秒查询率 QPS (Query Per Second)、响应时间(系统对请求作出反应的时间)。举例来说,系统需要200 ms来处理 HTTP请求,而这个200 ms表示系统的响应时间。吞吐率:单位时间内处理的请求数。QPS:响应请求数/秒。这一指标与因特网领域的吞吐量差异并不那么明显。并行用户数:拥有使用系统功能正常使用的用户数。举例来说,一个即时通信系统,它在一定程度上代表系统的并发用户数量。

4.在 java中,high-currency属于编程术语,意思是大量用户正在访问,导致系统数据不正确、处理数据的现象。并行意味着多个线程或进程可以同时处理不同的操作。要想使系统能够适应高并发状态,需要从各个方面进行系统优化,包括:硬件、网络、系统架构、开发语言的选择、数据结构的使用、算法的优化等。

高并发是什么意思?

高并发(High Concurrency),通常是指通过设计保证系统能够同时并行处理很多请求。通俗来讲,高并发是指在同一个时间点,有很多用户同时的访问同一 API 接口或者 Url 地址。它经常会发生在有大活跃用户量,用户高聚集的业务场景中。

什么是高并发?

高并发(High Concurrency)是一种系统运行过程中遇到的一种“短时间内遇到大量操作请求”的情况,主要发生在web系统集中大量访问收到大量请求(例如:12306的抢票情况;天猫双十一活动)。该情况的发生会导致系统在这段时间内执行大量操作,例如对资源的请求,数据库的操作等。

高并发的处理指标?

高并发相关常用的一些指标有:

1.响应时间(Response Time)

响应时间:系统对请求做出响应的时间。例如系统处理一个HTTP请求需要200ms,这个200ms就是系统的响应时间

2.吞吐量(Throughput)

吞吐量:单位时间内处理的请求数量。

3.每秒查询率QPS(Query Per Second)

QPS:每秒响应请求数。在互联网领域,这个指标和吞吐量区分的没有这么明显。

4.并发用户数

并发用户数:同时承载正常使用系统功能的用户数量。例如一个即时通讯系统,同时在线量一定程度上代表了系统的并发用户数。

高并发和多线程的关系和区别

“高并发和多线程”总是被一起提起,给人感觉两者好像相等,实则 高并发 ≠ 多线程

1.多线程

多线程是java的特性,因为现在cpu都是多核多线程的,可以同时执行几个任务,为了提高jvm的执行效率,java提供了这种多线程的机制,以增强数据处理效率。多线程对应的是cpu,高并发对应的是访问请求,可以用单线程处理所有访问请求,也可以用多线程同时处理访问请求。

在过去单CPU时代,单任务在一个时间点只能执行单一程序。之后发展到多任务阶段,计算机能在同一时间点并行执行多任务或多进程。虽然并不是真正意义上的“同一时间点”,而是多个任务或进程共享一个CPU,并交由操作系统来完成多任务间对CPU的运行切换,以使得每个任务都有机会获得一定的时间片运行。

再后来发展到多线程技术,使得在一个程序内部能拥有多个线程并行执行。一个线程的执行可以被认为是一个CPU在执行该程序。当一个程序运行在多线程下,就好像有多个CPU在同时执行该程序。

总之,多线程即可以这么理解:多线程是处理高并发的一种编程方法,即并发需要用多线程实现。

2.高并发

高并发不是JAVA的专有的东西,是语言无关的广义的,为提供更好互联网服务而提出的概念。

典型的场景,例如:12306抢火车票,天猫双十一秒杀活动等。该情况的发生会导致系统在这段时间内执行大量操作,例如对资源的请求,数据库的操作等。如果高并发处理不好,不仅仅降低了用户的体验度(请求响应时间过长),同时可能导致系统宕机,严重的甚至导致OOM异常,系统停止工作等。

如果要想系统能够适应高并发状态,则需要从各个方面进行系统优化,包括,硬件、网络、系统架构、开发语言的选取、数据结构的运用、算法优化、数据库优化等……而多线程只是其中解决方法之一。

多线程并发技术

1.并发编程三要素

  •  原子性原子,即一个不可再被分割的颗粒。在Java中原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
  •  有序性程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
  •  可见性当多个线程访问同一个变量时,如果其中一个线程对其作了修改,其他线程能立即获取到最新的值。

2. 线程的五大状态

  •  创建状态当用 new 操作符创建一个线程的时候
  •  就绪状态调用 start 方法,处于就绪状态的线程并不一定马上就会执行 run 方法,还需要等待CPU的调度
  •  运行状态CPU 开始调度线程,并开始执行 run 方法
  •  阻塞状态线程的执行过程中由于一些原因进入阻塞状态比如:调用 sleep 方法、尝试去得到一个锁等等
  •  死亡状态run 方法执行完 或者 执行过程中遇到了一个异常

3.悲观锁与乐观锁

  •  悲观锁:每次操作都会加锁,会造成线程阻塞。
  •  乐观锁:每次操作不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止,不会造成线程阻塞。

4.线程之间的协作:wait/notify/notifyAll等

5.synchronized 关键字

6.CAS

CAS全称是Compare And Swap,即比较替换,是实现并发应用到的一种技术。操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。

7.线程池

如果我们使用线程的时候就去创建一个线程,虽然简单,但是存在很大的问题。如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。线程池通过复用可以大大减少线程频繁创建与销毁带来的性能上的损耗。

高并发技术方案:提高高并发能力

1.分布式缓存:redis、memcached等,结合CDN来解决图片文件等访问。

2.消息队列中间件:activeMQ等,解决大量消息的异步处理能力。

3.应用拆分:一个工程被拆分为多个工程部署,利用dubbo解决多工程之间的通信。

4.数据库垂直拆分和水平拆分(分库分表)等。

5.数据库读写分离,解决大数据的查询问题。

6.还可以利用nosql ,例如mongoDB配合mysql组合使用。

7.还需要建立大数据访问情况下的服务降级以及限流机制等。

同步、异步 阻塞、非阻塞

 同步:执行一个操作之后,等待结果,然后才继续执行后续的操作。

异步:执行一个操作后,可以去执行其他的操作,然后等待通知再回来执行刚才没执行完的操                  作。

阻塞:进程给CPU传达一个任务之后,一直等待CPU处理完成,然后才执行后面的操作。

非阻塞:进程给CPU传达任务后,继续处理后续的操作,隔断时间再来询问之前的操作是否成。                  这样的过程其实也叫轮询。

注意:异步必定是非阻塞的,所以不存在异步阻塞和异步非阻塞的说法。

同步异步阻塞和非阻塞的区别理解

一、同步和异步
同步和异步是一种 消息通知机制

       同步:发出一次请求后必须等到该请求有返回结果,才能继续下一步工作;请求者需主动询                     问是否有返回结果;
      异步:发出一次请求后无需等到该请求有返回结果,即可进行下一步工作;请求有结果后,会                   以某种方式通知请求者;
同步和异步是相对于操作结果来说,会不会等待结果返回

异步一般通过状态、通知和回调来通知调用者。
状态:即监听被调用者的状态(轮询),调用者需要每隔一定时间检查一次,效率会很低。
通知:当被调用者执行完成后,发出通知告知调用者,无需消耗太多性能。
回调:与通知类似,当被调用者执行完成后,会调用调用者提供的回调函数

场景比喻:
举个例子,去银行办理业务,可能会有两种方式:排队等候和排号等别人通知

     1.前者(排队等候)就是同步等待消息通知,也就是我要一直在等待银行办理业务情况;
     2.后者(排号,等待别人通知)就是异步等待消息通知。在异步消息处理中,等待消息通知者(在这个例子中就是等待办理业务的人)往往注册一个回调机制,在所等待的事件被触发时由触发机制(在这里是柜台的人)通过某种机制(在这里是写在小纸条上的号码,喊号)找到等待该事件的人。

二、阻塞和非阻塞
阻塞、非阻塞,是程序等待调用结果时的状态;

阻塞:发出一次请求后,在未得到返回结果前,线程挂起,这期间线程无法做其他事情;
非阻塞:发出一次请求后,在未得到返回结果前,线程不会挂起,这期间线程可以做其他事情;
阻塞和非阻塞这两个概念与程序(线程)等待消息通知(无所谓同步或者异步)时的状态有关。也就是说阻塞与非阻塞主要是程序(线程)等待消息通知时的状态角度来说的。

场景比喻:

阻塞。继续上面的那个例子,不论是排队还是使用号码等待通知,如果在这个等待的过程中,等待者除了等待消息通知之外不能做其它的事情,那么该机制就是阻塞的,表现在程序中,也就是该程序一直阻塞在该函数调用处不能继续往下执行。

非阻塞。相反,有的人喜欢在银行办理这些业务的时候一边打打电话发发短信一边等待,这样的状态就是非阻塞的,因为他(等待者)没有阻塞在这个消息通知上,而是一边做自己的事情一边等待。

同步异步阻塞非阻塞总结:

同步机制 发送方发送请求之后,需要等接收方发回响应后才接着发
异步机制 发送方发送一个请求之后不等待接收方响应这个请求,就继续发送下个请求。
阻塞调用调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回,该线程在此过程中不能进行其他处理
非阻塞调用调用结果不能马上返回,当前线程也不会被挂起,而是立即返回执行下一个调用。(网络通信中主要指的是网络套接字Socket的阻塞和非阻塞方式,而soket 的实质也就是IO操作)

区分开同步异步阻塞非阻塞,同步异步说的是被调用者结果返回时通知进程的一种通知机制,阻塞非阻塞说的是调用结果返回前进程的状态,是挂起还是继续处理其他任务。

三、同步异步阻塞非阻塞组合应用
同步阻塞方式 发送方向接收方发送请求后,一直等待响应;接收方处理请求时进行的IO操作如果不能马上得到结果,就一直等到返回结果后,才响应发送方,期间不能进行其他工作。这种方式实现简单,但是效率最低。

同步非阻塞方式发送方向接收方发送请求后,一直等待响应;接收方处理请求时进行的IO操作如果不能马上得到结果,就立即返回,去做其他事情,但由于没有得到请求处理结果,不响应发送方,发送方一直等待。一直到IO操作完成后,接收方获得结果响应发送方后,接收方才进入下一次请求过程。在实际中不使用这种方式。

异步阻塞方式发送方向接收方发送请求后,不用等待响应,可以接着进行其他工作;接收方处理请求时进行的IO操作如果不能马上得到结果,就一直等到返回结果后,才响应发送方,期间不能进行其他工作。这种方式在实际中也不使用。

异步非阻塞方式发送方向接收方请求后,不等待响应,可以继续其他工作,接收方处理请求时进行IO操作如果不能马上得到结果,也不等待,而是马上返回取做其他事情。当IO操作完成以后,将完成状态和结果通知接收方,接收方在响应发送方。效率最高

场景比喻:

对上面所讲的概念再次进行一个场景梳理,上面已经明确说明,同步/异步关注的是消息通知的机制,而阻塞/非阻塞关注的是程序(线程)等待消息通知时的状态。

以小明下载文件打个比方,从这两个关注点来再次说明这两组概念,希望能够更好的促进大家的理解。

1. 同步阻塞:
小明一直盯着下载进度条,到 100% 的时候就完成。
①同步体现在:等待下载完成通知;
②阻塞体现在:等待下载完成通知过程中,不能做其他任务处理;

2. 同步非阻塞:
小明提交下载任务后就去干别的,每过一段时间就去瞄一眼进度条,看到 100% 就完成。
①同步体现在:等待下载完成通知;
②非阻塞体现在:等待下载完成通知过程中,去干别的任务了,只是时不时会瞄一眼进度条;小明必须要在两个任务间切换,关注下载进度

3. 异步阻塞:
小明换了个有下载完成通知功能的软件,下载完成就“叮”一声。不过小明仍然一直等待“叮”的声音(看起来很傻)。
①异步体现在:下载完成“叮”一声通知;
②阻塞体现在:等待下载完成“叮”一声通知过程中,不能做其他任务处理;

4. 异步非阻塞:
仍然是那个会“叮”一声的下载软件,小明提交下载任务后就去干别的,听到“叮”的一声就知道完成了。
①同步体现在:下载完成“叮”一声通知;
②非阻塞体现在:等待下载完成“叮”一声通知过程中,去干别的任务了,只需要接收“叮”声通知即可;软件处理下载任务,小明处理其他任务,不需关注进度,只需接收软件“叮”声通知,即可

也就是说,同步/异步是“下载完成消息”通知的方式(机制),而阻塞/非阻塞则是在等待“下载完成消息”通知过程中的状态(能不能干其他任务),在不同的场景下,同步/异步、阻塞/非阻塞的四种组合都有应用。

所以,综上所述,同步和异步仅仅是关注的消息如何通知的机制,而阻塞与非阻塞关注的是等待消息通知时的状态。也就是说,同步的情况下,是由处理消息者自己去等待消息是否被触发,而异步的情况下是由触发机制来通知处理消息者,

同步、异步:
概念:消息的通知机制
解释:涉及到IO通知机制;所谓同步,就是发起调用后,被调用者处理消息,必须等处理完才直接返回结果,没处理完之前是不返回的,调用者主动等待结果;所谓异步,就是发起调用后,被调用者直接返回,但是并没有返回结果,等处理完消息后,通过状态、通知或者回调函数来通知调用者,调用者被动接收结果。

阻塞、非阻塞:
概念:程序等待调用结果时的状态
解释:涉及到CPU线程调度;所谓阻塞,就是调用结果返回之前,该执行线程会被挂起,不释放CPU执行权,线程不能做其它事情,只能等待,只有等到调用结果返回了,才能接着往下执行;所谓非阻塞,就是在没有获取调用结果时,不是一直等待,线程可以往下执行,如果是同步的,通过轮询的方式检查有没有调用结果返回,如果是异步的,会通知回调。

经典故事案例:

  • 人物:老张
  • 道具:普通水壶(水烧开不响);响水壶(水烧开发出响声)
  • 案例

 1、同步阻塞:
       老张在厨房用普通水壶烧水,一直在厨房等着(阻塞),盯到水烧开(同步);
 2、异步阻塞:
       老张在厨房用响水壶烧水,一直在厨房中等着(阻塞),直到水壶发出响声(异步),老张知道水烧开了;
 3、同步非阻塞:
       老张在厨房用普通水壶烧水,在烧水过程中,就到客厅去看电视(非阻塞),然后时不时去厨房看看水烧开了没(轮询检查同步结果);
 4、异步非阻塞:
       老张在厨房用响水壶烧水,在烧水过程中,就到客厅去看电视(非阻塞),当水壶发出响声(异步),老张就知道 水烧开了。

你可能感兴趣的:(多线程,java,开发语言)