美文网首页美文共赏
并发--基本概念

并发--基本概念

作者: zhemehao819 | 来源:发表于2021-12-10 22:24 被阅读0次

一、基本概念

1、进程与线程

  • 进程: 资源分配的最小单位。进程是线程的容器, 一个进程中包含多个线程, 真正执行任务的是线程
  • 线程: 资源调度的最小单位

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)

线程

  • 一个进程之内可以分为一到多个线程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行 。
  • Java 中,线程作为小调度单位,进程作为资源分配的小单位。 在 windows 中进程是不活动的,只是作 为线程的容器

二者对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享;
  • 进程间通信较为复杂
    -- 同一台计算机的进程通信称为 IPC(Inter-process communication)
    -- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

2、并发与并行

并发 (concurrent)

  • 微观串行, 宏观并行
  • 在单核 cpu下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于cpu 在线程间(时间片很短)的切换非常快,给人的 感觉是同时运行的 。一般会将这种线程轮流使用 CPU的做法称为并发(concurrent)
  • 将线程轮流使用cpu称为并发(concurrent)
1583408729416

并行

  • 多核 cpu下,每个核(core) 都可以调度运行线程,这时候线程可以是并行的,不同的线程同时使用不同的cpu在执行。
    1583408812725

二者对比

  • 并发(concurrent): 是同一时间应对(dealing with)多件事情的能力
  • 并行(parallel): 是同一时间动手做(doing)多件事情的能力例子

3、同步和异步

以调用方的角度讲

  • 如果需要等待结果返回才能继续运行的话就是同步
  • 如果不需要等待就是异步

1 设计

多线程可以让方法执行变为异步的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如果没有线程调度机制,这5秒cpu什么都做不了,其它代码都得暂停。

2 结论

  • 比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程
  • tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程
  • UI 程序中,开线程进行其他操作,避免阻塞 UI 线程

二、Java线程

1、线程的创建

方法1、继承于Thread类,重写run()方法。

public class CreateThread {
    public static void main(String[] args) {
        Thread myThread = new MyThread();
        // 启动线程
        myThread.start();
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("my thread running...");
    }
}

缺点:Java不支持多继承,如果继承了Thread类,那么就不能再继承其他类。另外任务与线程代码耦合,当多个线程执行一样的任务时需要多份任务代码。

下面是使用匿名内部类的方式创建:

public static void main(String[] args) {
        // 匿名内部类方式创建 Thread
        Thread t = new Thread("t1") {
            @Override
            public void run() {
                log.debug("running");
            }
        };
        
        t.start();
        log.debug("running");
    }

方法2、使用Runnable配合Thread(推荐)

实现Runnable接口,并且实现run()方法。在创建线程时作为参数传入该类的实例即可。

public class CreateThread2 {

   private static class MyRunnable implements Runnable {
      @Override
      public void run() {
         System.out.println("my runnable running...");
      }
   }

   public static void main(String[] args) {
      Thread thread = new Thread(new MyRunnable());
      thread.start();
   }
}

使用lambda表达式简化
当一个接口带有@FunctionalInterface注解时,是可以使用lambda来简化操作的
所以方法二中的代码可以被简化为:

public class Test2 {
    public static void main(String[] args) {
        //创建线程任务
        Runnable r = () -> {
            //直接写方法体即可
            System.out.println("Runnable running");
            System.out.println("Hello Thread");
        };
        //将Runnable对象传给Thread
        Thread t = new Thread(r);
        t.start();
    }
}

通过查看源码可以发现,方法2其实还是通过使用 Thread 类中的 run 方法执行的,Thread类中的run方法是先判断target(Runnable实现类)不为空时就调用target中的run方法。

方法3、通过Callable和FutureTask创建线程

  • 实际上FutureTask继承了Runnable,可以作为线程Thread对象的构造参数
  • Callable接口里也只有一个call方法,可以用lamada表达式简化
public class Test3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //需要传入一个Callable对象
        FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("线程执行!");
                Thread.sleep(1000);
                return 100;
            }
        });

        Thread r1 = new Thread(task, "t2");
        r1.start();
        //获取线程中方法执行后的返回结果
        System.out.println(task.get());
    }
}

方法4、使用线程池

class NumberThread implements Runnable{
    @Override
    public void run() {
        for(int i = 0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

class Number2Thread implements Callable {
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for(int i = 1;i<=10;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
            sum+=i;
        }
        return sum;
    }
}

public class ThreadPool {
    public static void main(String[] args) {
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);//创建一个可重用固定线程数为10的线程池

        //查看该对象是哪个类造的
        System.out.println(service.getClass());//class java.util.concurrent.ThreadPoolExecutor
        //设置线程池的属性
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();

        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合使用于Runnable
        Future future = service.submit(new Number2Thread());//适合使用于Callable
        try {
            System.out.println(future.get());//输出返回值
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        //3.关闭连接池
        service.shutdown();
    }
}

runnable 和 callable 有什么区别?

相同点:

  • 都是接口
  • 都可以编写多线程程序
  • 都采用Thread.start()启动线程

主要区别:

  • Runnable 接口 run 方法无返回值;
  • Callable 接口 call 方法有返回值,支持泛型,和Future、FutureTask配合可以用来获取异步执行的结果
  • Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息
  • 注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。

2、线程的运行原理

栈与栈帧

Java Virtual Machine Stacks (Java 虚拟机栈)
我们都知道 JVM 内存由堆、栈、方法区所组成,其中栈内存是给谁用的呢?

  • 其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

线程上下文切换(Thread Context Switch

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Context Switch 频繁发生会影响性能

3、常用方法

(1)start() vs run()

  • 被创建的Thread对象直接调用重写的run方法时, run方法是在主线程中被执行的,这个线程对象会处一直处在新建状态。
  • 使用 start 方式,CPU 会为创建的线程分配时间片,线程进入运行状态,然后线程调用 run 方法执行逻辑。

(2)sleep()与yield()

sleep (使线程阻塞)

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞),可通过state()方法查看;
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行
  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 。如:
    //休眠一秒
    TimeUnit.SECONDS.sleep(1);
    //休眠一分钟
    TimeUnit.MINUTES.sleep(1);
    

在while(true)的情况下可以使用sleep来避免cpu空转(持续100%),sleep可以使当前线程状态变成time_waiting状态。(无需加锁就能实现)

yield (让出当前线程)

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态(仍然有可能被执行),然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器

线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它

  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

  • 设置方法:

    thread1.setPriority(Thread.MAX_PRIORITY); //设置为优先级最高
    

(3)join()方法

用于等待某个线程结束。哪个线程内调用join()方法,就等待哪个线程结束,然后再去执行其他线程。
如在主线程中调用ti.join(),则是主线程等待t1线程结束

Thread thread = new Thread();
//等待thread线程执行结束
thread.join();
//最多等待1000ms,如果1000ms内线程执行完毕,则会直接执行下面的语句,不会等够1000ms
thread.join(1000);

(4)interrupt()方法

用于打断阻塞(sleep wait join…)的线程。 处于阻塞状态的线程,CPU不会给其分配时间片。

每一个线程都有一个boolean类型的标志,此标志是当前的请求是否请求中断,默认为false。当一个线程A调用了线程B的interrupt方法时,那么线程B的是否请求的中断标志变为true。而线程B可以调用方法检测到此标志的变化。

  • 打断的是阻塞方法时(sleep、wait、join):如果线程B调用了阻塞方法,如果是否请求中断标志变为了true,那么它会抛出InterruptedException异常。抛出异常的同时它会将线程B的是否请求中断标志重置为false
  • 打断的是非阻塞方法时:只是会将打断标记为true,并不会对程序有其他影响,程序不会停止,会继续执行。如果要让线程在被打断后停下来,需要使用打断标记来判断
while(true) {
    if(Thread.currentThread().isInterrupted()) {
        break;
    }
}
  • 可以通过线程B的isInterrupted方法进行检测是否请求中断标志,另外还有一个静态的方法interrupted方法也可以检测标志,但是静态方法它检测完以后会自动的将是否请求中断标志位置为false。
//用于查看打断标记,返回值被boolean类型
t1.isInterrupted();
//静态方法检测中断标志,并检测完后重置false
Thread.interrupted();

interrupt方法的应用——两阶段终止模式

当我们在执行线程一时,想要终止线程二,这是就需要使用interrupt方法来优雅的停止线程二。

代码:

public class Test7 {
    public static void main(String[] args) throws InterruptedException {
        Monitor monitor = new Monitor();
        monitor.start();
        Thread.sleep(3500);
        monitor.stop();
    }
}

class Monitor {

    Thread monitor;

    /**
     * 启动监控器线程
     */
    public void start() {
        //设置线控器线程,用于监控线程状态
        monitor = new Thread() {
            @Override
            public void run() {
                //开始不停的监控
                while (true) {
                    //判断当前线程是否被打断了
                    if(Thread.currentThread().isInterrupted()) {
                        System.out.println("处理后续任务");
                        //终止线程执行
                        break;
                    }
                    System.out.println("监控器运行中...");
                    try {
                        //线程休眠
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        //如果是在休眠的(打断的是阻塞方法)时候被打断,打断标记置为true,但抛出InterruptedException异常后又重置为false。所以此时需要再次设置为true
                        Thread.currentThread().interrupt();//非阻塞方法设置打断标记为true
                    }
                }
            }
        };
        monitor.start();
    }

    /**
     *  用于停止监控器线程
     */
    public void stop() {
        //打断线程
        monitor.interrupt();
    }
}

(5)不推荐使用的打断方法

  • stop方法 停止线程运行(可能造成共享资源无法被释放,其他线程无法使用这些共享资源)
  • suspend(暂停线程)/resume(恢复线程)方法

(6)守护线程

当JAVA进程中有多个线程在执行时,只有当所有非守护线程都执行完毕后,JAVA进程才会结束。但当非守护线程全部执行完毕后,守护线程无论是否执行完毕,也会一同结束。

//将线程设置为守护线程, 默认为false
monitor.setDaemon(true);

守护线程的应用

  • 垃圾回收器线程就是一种守护线程
  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 待它们处理完当前请求

4、线程的状态

(1)五种状态

操作系统 层面来描述:

  • 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联(例如线程调用了start方法)
  • 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
  • 【运行状态】指获取了 CPU 时间片运行中的状态
    -当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
  • 【阻塞状态】
    -如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】
    -等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    -与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
  • 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

(2)六种状态

Java API 层面来描述:
根据 Thread.State 枚举,分为六种状态

  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,如sleep就位TIMED_WAITING, join为WAITING状态。后面会在状态转换一节详述。
  • TERMINATED 当线程代码运行结束

演示线程6中状态:

@Slf4j(topic = "heyuanjun.test")
public class Thread6StatusCode {

    public static void main(String[] args) {

        //NEW
        Thread t1 = new Thread(() -> {
            log.info("----t1-----");
        }, "t1");

        //RUNNABLE
        Thread t2 = new Thread(() -> {
            while (true) {
                // log.info("----t2-----");
            }
        }, "t2");
        t2.start();

        //TERMINATED
        Thread t3 = new Thread(() -> {
            log.info("----t3-----");
        }, "t3");
        t3.start();

        //time_waiting
        Thread t4 = new Thread(() -> {
            synchronized (Thread6StatusCode.class) {
                try {
                    Thread.sleep(10 * 1000);
                    log.info("----t4-----");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t4.start();

        //waiting
        Thread t5 = new Thread(() -> {
            try {
                t2.join();
                log.info("----t5-----");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t5.start();

        //blocked
        Thread t6 = new Thread(() -> {
            synchronized (Thread6StatusCode.class) {
                log.info("----t6-----");
            }
        });
        t6.start();


        try {
            Thread.sleep(2 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("t1 线程状态:{}", t1.getState());
        log.info("t2 线程状态:{}", t2.getState());
        log.info("t3 线程状态:{}", t3.getState());
        log.info("t4 线程状态:{}", t4.getState());
        log.info("t5 线程状态:{}", t5.getState());
        log.info("t6 线程状态:{}", t6.getState());
        log.info("============结束==============");
    }
}

执行结果:

13:39:38.408 [t3] INFO heyuanjun.test - ----t3-----
13:39:40.406 [main] INFO heyuanjun.test - t1 线程状态:NEW
13:39:40.408 [main] INFO heyuanjun.test - t2 线程状态:RUNNABLE
13:39:40.408 [main] INFO heyuanjun.test - t3 线程状态:TERMINATED
13:39:40.408 [main] INFO heyuanjun.test - t4 线程状态:TIMED_WAITING
13:39:40.408 [main] INFO heyuanjun.test - t5 线程状态:WAITING
13:39:40.408 [main] INFO heyuanjun.test - t6 线程状态:BLOCKED
13:39:40.408 [main] INFO heyuanjun.test - ============结束==============
13:39:48.405 [t4] INFO heyuanjun.test - ----t4-----
13:39:48.405 [t6] INFO heyuanjun.test - ----t6-----

相关文章

网友评论

    本文标题:并发--基本概念

    本文链接:https://www.haomeiwen.com/subject/roehfrtx.html