• 多线程入门


    多线程简介

    1. 进程/线程

    # 进程
    - 进程由指令和数据组成,指令要运行,数据要读写,必须将指令加载到CPU,数据加载到内存。指令运行过程中还需要用到磁盘,网络等IO设备
    - 进程用来加载指令,管理内存,管理IO
    - 一个程序被运行,从磁盘加载这个程序的代码到内存,就是开启了一个进程
    - 进程可以视为一个程序的实例
    - 大部分程序可以运行多个实例进程,也有的程序只能启动一个实例进程
    
    # 线程
    - 一个进程内部包含1-n个线程
    - 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
    - JVM中,进程作为资源分配的最小单元,线程作为最小调度单元
    
    # 对比
    - 进程基本上相互独立                                    线程存在于进程内,是进程的一个子集
    - 进程拥有共享的资源,如内存空间,供其内部的线程共享
    - 进程间通信比较复杂: 同一台计算机的进程通信为IPC(Inter-process communication), 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,如HTTP
    - 线程通信比较简单,因为它们共享进程内的内存,如多个线程可以访问同一个共享变量
    - 线程更轻量,线程上下文切换成本一般比进程上下文切换低
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    2. 并行/并发

    2.1 并发

    - 单核cpu下,线程是串行
    - 任务调度器: 操作系统组件,将cpu的时间片(windows下为15ms)分给不同的线程使用
    - cpu在线程间的切换非常快,感觉就是各个线程是同时运行的
    - 微观串行,宏观并行
    
    - concurrent:cpu核心同一个时间应对多个线程的能力
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2.2 并行

    - 多核cpu下,每个核都可以调度运行线程,这个时候线程就是并行的
    
    - parrel: 同一个时间内,cpu真正去执行多个线程的能力
    - 其实很多时候,并发和并行是同时存在的
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述

    3. 多线程应用场景

    # 异步调用
    - 某个方法的执行,不需要必须立刻获取返回结果
    - 某个耗时业务,如果用另外一个线程来做,不会阻塞主线程的业务
    
    # 提升效率
     - 一个任务,可以拆分为不同的任务,不同任务间互不依赖
    - 多核cpu: 一个进程在进行多个独立操作时,没必要将其放到一个线程中顺序执行,创建多个线程分别执行,这样就会分到更多的cpu,执行更快
    - 单核cpu: 没必要分成多个线程,因为依然会轮流执行,还会有上下文切换的损失
    - java中要实现异步调用,必须采用多线程的方式
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    4. 创建线程

    • 启动JVM(main方法),就是开启了一个JVM进程
    • JVM进程内包含一个主线程,主线程可以派生出多个其他线程。同时还有一些守护线程,如垃圾回收线程
    • 主线程,守护线程,派生线程,cpu随机分配时间片,交替随机执行

    4.1. 继承Thread类

    • 继承 Thread类,重写run(),start()启动线程
    • 两种写法:直接继承,匿名内部类
    • 在主线程内部开启了一个新的线程,在一个java进程中创造了一个其他线程
    // 基本写法
    package com.nike.erick.d01;
    
    public class Demo01 {
        public static void main(String[] args) {
            ErickThread erickThread = new ErickThread();
            erickThread.start();
    
            System.out.println("Main Thread Running");
        }
    }
    
    class ErickThread extends Thread {
    
        @Override
        public void run() {
            System.out.println("Erick Thread Running");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    // 匿名内部类
    package com.nike.erick.d01;
    
    public class Demo02 {
        public static void main(String[] args) {
            Thread firstThread = new Thread() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + ": Erick Thread");
                }
            };
    
            firstThread.start();
            System.out.println("Erick Main Running");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    4.2 实现Runnable接口

    • 将要启动的线程和要执行的任务分开,更加灵活
    • 实现Runnable接口,并将该对象作为Thread构造方法的参数传递
    • 三种方式: 直接实现,匿名内部类,lambda
    - Runnable把线程和任务分开了
    - Runnable更加容易和线程池相关的API结合使用
    - Runnable让任务脱离了继承体系,更加灵活
    - Runnable避免单继承的问题
    
    • 1
    • 2
    • 3
    • 4
    // 实现Runnable接口
    package com.nike.erick.d01;
    
    public class Demo03 {
        public static void main(String[] args) {
            Thread firstThread = new Thread(new LucyThread(), "t1");
            firstThread.start();
            System.out.println("Main Thread Running");
        }
    }
    
    class LucyThread implements Runnable {
    
        @Override
        public void run() {
            System.out.println("Lucy Thread Running");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    // 匿名内部类
    package com.nike.erick.d01;
    
    public class Demo04 {
        public static void main(String[] args) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("Lucy Thread");
                }
            }, "t1");
    
            thread.start();
    
            System.out.println("Main Thread");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    // Lambda 方法
    package com.nike.erick.d01;
    
    public class Demo04 {
        public static void main(String[] args) {
            Thread thread = new Thread(() -> System.out.println("Lucy Thread"), "t1");
    
            thread.start();
    
            System.out.println("Main Thread");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    Runnable vs Thread

    Runnable
    # 策略模式
    - 实际执行时候是调用的Runnable接口的run方法
    - 因为将Runnable实现类传递到了Thread的构造参数里面
    
    • 1
    • 2
    • 3
    • Runnable接口
    // @FunctionalInterface修饰的接口,可以用lambda来创建
    @FunctionalInterface
    public interface Runnable {
        public abstract void run();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • Thread 类
    public class Thread implements Runnable {
    
        private Runnable target;
        
        public Thread(Runnable target) {
            this(null, target, "Thread-" + nextThreadNum(), 0);
        }
    
        @Override
        public void run() {
            if (target != null) {
                target.run();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    Thread
    • Thread的实现类重写了run方法,因此在通过Thread的start方法调用的时候,实际是调用了实现类的run方法

    4.3 FutureTask接口

    • 可以获取任务执行结果的一个接口
    # FutureTask 继承关系
    class FutureTask<V> implements RunnableFuture<V>
    interface RunnableFuture<V> extends Runnable, Future<V>
    interface Future<V> :
                           boolean cancel()
                           boolean isCancelled()
                           boolean isDone()
                           V get()
                           V get(long timeout, TimeUnit unit)
    
    # Callable 接口实现类
    interface Callable<V>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    package com.nike.erick.d01;
    
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    import java.util.concurrent.TimeUnit;
    
    public class Demo06 {
        public static void main(String[] args) {
    
            FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    System.out.println("slave-thread running");
                    TimeUnit.SECONDS.sleep(2);
                    return "success from erick";
                }
            });
    
            Thread thread = new Thread(futureTask, "erick-thread");
            thread.start();
    
            try {
                /*获取结果的时候,会将主线程阻塞*/
                System.out.println("slave-thread result: " + futureTask.get());
                System.out.println("slave-thread result: " + futureTask.isCancelled());
                System.out.println("slave-thread result: " + futureTask.isDone());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
    
            System.out.println("Main Thread ending");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    5. 进程查看

    5.1. Linux

    # process status
    ps -ef
    ps -fe               # 进程查看
    
    ps -ef|grep java
    ps -fe|grep java     # 管道运算符, 查看java进程(包含java进程和查看的grep进程)
    
    kill pid             # 杀死指定进程
    top                  # 动态查看当前进程的占用cpu和mem情况。ctrl+c 退出
    
    top -H -P (pid)      # 查看某个进程内部的所有线程的cpu使用
                         # -H : 表示查看线程
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    5.2 Java

    JPS
    
    • 1

    5.3 线程工具JConsole

    • Java内置,检测java线程的图形化界面工具
    • 位于jdk/bin/目录下
    • 直接在Mac控制台输入: jconsole即可打开
    • 可以用来检测死锁

    6. 主线程和守护线程

    • main方法启动后,就会开启一个JVM, 包含一个主线程 和 守护线程, 其他线程
    • JVM就是一个java进程
    • 守护线程: 只要其他非守护线程运行结束了,即使守护线程的代码没执行完,也会强制退出
    • 垃圾回收器就是一种守护线程
    package com.nike.erick.d01;
    
    import java.util.concurrent.TimeUnit;
    
    /* 输出结果: Dame Starting
                Main Thread ending*/
    public class Demo08 {
        public static void main(String[] args) throws InterruptedException {
            Thread daemonThread = new Thread(() -> {
                try {
                    System.out.println("Dame Starting");
                    TimeUnit.SECONDS.sleep(10);
                    System.out.println("Dame");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
    
            daemonThread.setDaemon(true);
            daemonThread.start();
    
            TimeUnit.SECONDS.sleep(2);
            System.out.println("Main Thread ending");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    7. JVM 模型

    7.1 栈与栈帧

    • Java Virtual Machine Stacks: Java虚拟机栈内存
    • 栈内存:虚拟机启动后,每个线程启动后,都会分配一块独立的栈内存
    • 栈帧: 每个栈包含多个栈帧(Frames),对应着每次方法调用时所占的内存
    • 活动栈帧:每个线程只能有一个活动栈帧,对应着当前正在执行的方法
    • 弹栈:方法执行完毕后,栈帧内存依次弹栈,栈内存销毁

    在这里插入图片描述

    7.2 Thread Context Switch

    • 因为一些原因导致cpu不再执行当前线程,转而执行另一个线程代码
    1. 线程的cpu时间片用完
    2. 垃圾回收:               # 垃圾回收的时候,其他所有线程要暂停, STW
    3. 有更高优先级的线程需要运行
    4. 线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法
    
    • 1
    • 2
    • 3
    • 4
    • 当发生线程上下文切换时候,需要操作系统保存当前线程的状态,并恢复另一个线程的状态
    • 程序计数器:java中的, 线程切换时,记住下一条jvm指令的执行地址,是线程私有的
    • 状态包含:程序计数器,虚拟机中每个栈帧的信息,操作数栈,返回地址等
    • Thread Context Switch频繁发生会影响性能

    线程方法

    1. start/run

    public synchronized void start()
    public void run()
    
    1. start() :  
           1.1 线程从new状态转换为runnable状态,等待cpu分片从而有机会转换到running状态
           1.2 在主线程之外,再开启了一个线程
           1.3 已经start的线程,不能再次start  “IllegalThreadStateException”
           
    2. run():    
          2.1 线程内部调用start后实际执行的一个普通方法
          2.2 如果线程直接调用run() ,只是在主线程内,调用了一个普通的方法
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2. sleep/yield

    2.1 sleep

    public static native void sleep(long millis) throws InterruptedException
    
    1. 线程放弃cpu,从RUNNABLE 进入 TIMED_WAITING状态
    2. 睡眠中的线程可以自己去打断自己的休眠
    3. 不会放弃当前的锁资源
    4. 睡眠结束后,会变为RUNNABLE,并不会立即执行,而是等待cpu时间片的分配
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    package com.nike.erick.d02;
    
    import java.util.concurrent.TimeUnit;
    
    public class Demo01 {
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("slave-thread running");
                    try {
                        TimeUnit.SECONDS.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
    
            System.out.println("slave-thread before start: " + thread.getState()); //NEW
    
            thread.start();
            System.out.println("slave-thread after start: " + thread.getState()); // RUNNABLE
    
            TimeUnit.SECONDS.sleep(2);
            System.out.println("slave-thread while sleeping: " + thread.getState()); // TIMED_WAITING
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    package com.nike.erick.d02;
    
    import java.util.concurrent.TimeUnit;
    
    public class Demo02 {
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("slave-thread running");
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        System.out.println("slave-thread interrupted");
                        throw new RuntimeException("Interrupted Exception");
                    }
                    System.out.println("slave-thread ending"); // 不会执行
                }
            });
    
            thread.start();
            TimeUnit.SECONDS.sleep(1);
            thread.interrupt(); // 打断睡眠中的线程
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    2.2 yield

    public static native void yield()
    
    - 线程让出cpu资源,让其他线程先去执行
    - 让线程从RUNNING状态转换为RUNNABLE状态
    - 假如其他线程不用cpu,那么cpu又会分配时间片到当前线程,可能压根就没停下
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.3 区别

    sleep: 让当前线程从 RUNNING --> Timed Waiting(阻塞), 睡眠结束后会进入 RUNNABLE状态
    yield: 让当前线程从 RUNNING --> RUNNABLE 状态,然后调度其他线程
    
    • 1
    • 2
    priority及yield
    package com.nike.erick.d02;
    
    public class Demo03 {
        public static void main(String[] args) {
            Thread firstThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    int i = 0;
                    while (true) {
                        i++;
                        System.out.println("T1--->" + i);
                    }
                }
            });
    
            Thread secondThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    int i = 0;
                    while (true) {
                    Thread.yield();// 可以放弃当前线程
                        i++;
                        System.out.println("        T2--->" + i);
                    }
                }
            });
    
            firstThread.setPriority(10);
            firstThread.start();
            secondThread.setPriority(1);
            secondThread.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    sleep应用
    • 程序一直执行,浪费cpu资源
    package com.dreamer.multithread.day02;
    
    public class Demo01 {
        public static void main(String[] args) {
            while (true) {
                System.out.println("i am working");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 程序间歇性休眠,让出cpu资源, 避免空转
    package com.dreamer.multithread.day02;
    
    import java.util.concurrent.TimeUnit;
    
    public class Demo01 {
        public static void main(String[] args) throws InterruptedException {
            while (true) {
                TimeUnit.SECONDS.sleep(1);
                System.out.println("i am working");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    3. Join

    • 谁调用 join,就等谁的线程结束后再去运行当前线程

    3.1. join()

    • 等待当前线程执行完毕
    public final void join() throws InterruptedException
    
    • 1
    单个线程
    package com.dreamer.multithread.day02;
    
    import java.util.concurrent.TimeUnit;
    
    public class Demo02 {
        static int number = 0;
    
        public static void main(String[] args) {
            Thread slaveThread = new Thread("t1") {
                @Override
                public void run() {
                    try {
                        TimeUnit.SECONDS.sleep(4);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    number = 10;
                }
            };
    
            slaveThread.start();
            
            // t1线程调用join,t1线程就是插队执行
            try {
                slaveThread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("number: " + number);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    多个线程
    package com.dreamer.multithread.day02;
    
    import java.util.concurrent.TimeUnit;
    
    public class Demo03 {
        public static void main(String[] args) throws InterruptedException {
            Thread firstThread = new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
    
            Thread secondThread = new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
    
            firstThread.start();
            secondThread.start();
    
            /**
             * 1. 两个线程同时插队,以相同优先级执行
             * 2. 所以一共等待时间为2s
             */
            long begin = System.currentTimeMillis();
            firstThread.join();
            secondThread.join();
            long end = System.currentTimeMillis();
    
            System.out.println("total time consuming: " + (end - begin));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    3.2 join(long millis)

    • 有时效的等待:最多等待多少ms, 0 代表永远执行完毕
    • 假如线程join的等待时间超过了实际执行时间,执行完后就可以不用继续等了
    public final synchronized void join(long millis) throws InterruptedException
    
    • 1
    package com.dreamer.multithread.day02;
    
    import java.util.concurrent.TimeUnit;
    
    public class Demo02 {
        static int number = 0;
    
        public static void main(String[] args) {
            Thread slaveThread = new Thread("t1") {
                @Override
                public void run() {
                    try {
                        TimeUnit.SECONDS.sleep(4);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    number = 10;
                }
            };
    
            slaveThread.start();
    
            // t1线程调用join,t1最多插队1秒,然后就继续执行当前线程
            try {
                slaveThread.join(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            // 0
            System.out.println("number: " + number);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    3.3 应用

    • 同步等待其他线程的结果调用

    4. interrupt

    # Thread 类
    
    # 1. 打断线程,谁调用打断谁
    public void interrupt()
    
    # 2. 判断线程是否被打断 : 默认值为true
             #  不会清除打断标记
    public boolean isInterrupted()
    
             #  会将打断标记置为false
    public static boolean interrupted()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    4.1 打断阻塞线程

    • 打断线程,抛出异常,并将线程打断标记重置为false(需要一点缓冲时间)
    • 如sleep,join,wait的线程,被打断的线程抛出错误
    package com.erick.multithread.d1;
    
    import java.util.concurrent.TimeUnit;
    
    public class Demo01 {
        public static void main(String[] args) throws InterruptedException {
            Thread slaveThread = new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
    
            slaveThread.start();
    
            TimeUnit.SECONDS.sleep(3);
    
            // false
            System.out.println(slaveThread.isInterrupted());
            slaveThread.interrupt();
            // 留下一点缓冲时间
            TimeUnit.SECONDS.sleep(3);
            // 阻塞的线程,被打断后,后通过异常的方式抛出,并将打断标记重制为false
            // false
            System.out.println(slaveThread.isInterrupted());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    4. 2 打断正常线程

    普通打断
    • 正常线程运行时,收到打断, 打断信号变为true,但不会做任何处理
    • 并不会直接终止打断线程,而是发出打断线程的请求
    package com.nike.erick.d02;
    
    import java.util.concurrent.TimeUnit;
    
    public class Demo05 {
        public static void main(String[] args) throws InterruptedException {
            Thread slaveThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        System.out.println("slave-thread running");
                    }
                }
            });
    
            slaveThread.start();
            TimeUnit.SECONDS.sleep(1);
    
            /*正常运行的线程,被打断后
             * 1. 继续正常运行
             * 2. 将打断标记置为 true*/
            slaveThread.interrupt();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    通过打断标记
    package com.nike.erick.d02;
    
    import java.util.concurrent.TimeUnit;
    
    public class Demo05 {
        public static void main(String[] args) throws InterruptedException {
            Thread slaveThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        /*判断打断标记*/
                        if (Thread.currentThread().isInterrupted()) {
                            break;
                        }
                        System.out.println("slave-thread running");
                    }
                }
            });
    
            slaveThread.start();
            TimeUnit.SECONDS.sleep(1);
    
            /*正常运行的线程,被打断后
             * 1. 继续正常运行
             * 2. 将打断标记置为 true*/
            slaveThread.interrupt();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    Two Phase Termination

    介绍
    • 终止正常执行的线程的优雅的模式:留下料理后事的机会
    # 1. 错误思路一:
    stop()   已经被废弃
       stop会真正kill线程,如果这时线程锁住了共享资源,
        那么当该线程被杀死后,再也没有机会去释放锁,其他线程将永远无法获取锁
    # 2. 错误思路二: 
    System.exit()
       目的仅仅是停止一个线程,但这种做法会将当前线程所在的整个java程序终止
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    业务场景
    • 监视线程,每隔2秒去执行监视流程,如果被打断,则中止监视流程
    package com.nike.erick.d02;
    
    import java.util.concurrent.TimeUnit;
    
    public class Demo06 {
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(new Runnable() {
    
                @Override
                public void run() {
                    while (true) {
                        if (Thread.currentThread().isInterrupted()) {
                            // 料理后事
                            closeJob();
                            break;
                        }
    
                        /*阶段一: 业务代码: 可能被打断*/
                        try {
                            executeBusiness();
                        } catch (Exception e) {
                            continue;
                        }
    
                        /*阶段二:休眠操作: 也可能被打断*/
                        try {
                            TimeUnit.SECONDS.sleep(2);
                        } catch (InterruptedException e) {
                            System.out.println("休眠时候被打断了: " + Thread.currentThread().isInterrupted());
                            // 如果休眠时被打断了,那么打断标记就变为了false, 需要再次打断,重制标记为为true
                            // 这里就类似于打断正常线程
                            Thread.currentThread().interrupt();
                        }
                    }
                }
            });
    
            thread.start();
    
            TimeUnit.SECONDS.sleep(4);
            thread.interrupt();
        }
    
        private static void executeBusiness() {
            System.out.println("执行了业务");
        }
    
        private static void closeJob() {
            System.out.println("料理后事");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    4.3 打断park线程

    • LockSupport: public static void park()
    • park当前的线程: 线程一直生存,直到被打断才会继续往下执行
    • 被打断后,打断标记就会变为true,就不能二次park了
    单次park
    package com.nike.erick.d02;
    
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.LockSupport;
    
    public class Demo07 {
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("thread start running....");
    
                    // 将当前线程停下来
                    LockSupport.park();
                    System.out.println("after first park...");
                }
            });
    
            thread.start();
            TimeUnit.SECONDS.sleep(2);
            // 打断后就会继续执行
            thread.interrupt();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    多次park
    package com.nike.erick.d02;
    
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.LockSupport;
    
    public class Demo08 {
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("slave thread running");
                    LockSupport.park();
                    System.out.println("after first park...");
    
                    // 获取当前线程的打断标记,同时将打断标记清除,即为 false
                    System.out.println("打断标记:" + Thread.interrupted());
    
                    LockSupport.park(); // 再次park
                    System.out.println("after second park...");
                }
            });
    
            thread.start();
            TimeUnit.SECONDS.sleep(1);
            thread.interrupt();
            TimeUnit.SECONDS.sleep(3);
            thread.interrupt();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    5. 其他方法

    ### 1. 成员方法
    
    # 1. 名字
    public final synchronized void setName(String name)  
    public final String getName()
    
    # 2. 优先级:最小为1,最大为10,默认为5
    #    只是一个cpu执行目标线程的顺序  建议设置,任务调度器可以忽略它进行分配资源
    public final void setPriority(int newPriority)
    public final int getPriority()
    
    # 3. 线程id: 13
    public long getId()
    
    # 4. 是否存活
    public final native boolean isAlive()
    
    # 5. 是否后台线程
    public final boolean isDaemon()
    
    # 6. 获取线程的状态
    public State getState()
    NEW  RUNNABLE  BLOCKED  WAITING   TIMED_WAITING   TERMINATED
    
    
    ### 2. 静态方法
    # 1. 获取当前线程
    public static native Thread currentThread()
    
    
    ## 3. 过时方法
    - stop:停止线程运行
    - suspend: 让线程暂停使用
    - resume: 恢复线程运行
    - 不推荐理由: 这三种方法,都可能造成死锁问题
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    生命周期

    1. 操作系统

    • 从操作系统层面来说,包含五种状态
    • cpu的时间片只会分给可运行状态的线程,阻塞状态的需要其本身阻塞结束

    image-20220925095436125

    NEW

    • new 出一个Thread的对象时,线程并没有创建,只是一个普通的对象
    • 调用start,new 状态进入runnable状态

    RUNNABLE

    • 调用start后,在jvm中创建了新的线程,但并不立即执行,只是处于就绪状态,即有资格执行
    • 等待cpu分配权限,只有轮到它的时候,才会真正执行

    RUNNING

    • 一旦cpu调度到了该线程,该线程才会真正开始执行
    # 该状态的线程可以转换为其他状态
    1. 进入TERMINATED:   stop(不推荐), 线程执行完毕, jvm crash
    2. 进入BLOCK:        sleep, wait,阻塞IO如网络数据读写, 获取某个锁资源
    3. 进入RUNNABLE:     cpu轮询到其他线程,线程主动yield放弃cpu
    
    • 1
    • 2
    • 3
    • 4

    BLOCK

    # 该状态的线程可以转换为其他状态
    1. 进入TERMINATED:   stop(不推荐), jvm crash
    2. 进入RUNNABLE:     线程阻塞结束,完成了指定时间的休眠
                         wait中的线程被其他线程notify/notifyall
                         获取到了锁资源
    
    • 1
    • 2
    • 3
    • 4
    • 5

    TERMINATED

    • 线程正常结束
    • 线程运行出错意外结束
    • jvm crash,导致所有的线程都结束

    2. JAVA层面

    • 根据Thread类中内部State枚举,分为六种状态

    线程安全

    1. 线程不安全

    1.1 共享变量

    • 多个线程对共享变量的并发修改,导致结果并不像预期中那样
    package com.nike.erick.d03;
    
    public class Demo01 {
        private static int number = 0;
    
        public static void main(String[] args) throws InterruptedException {
            Thread firstThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 100000; i++) {
                        number++;
                    }
                }
            });
    
            Thread secondThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 100000; i++) {
                        number--;
                    }
                }
            });
    
            firstThread.start();
            secondThread.start();
    
            firstThread.join();
            secondThread.join();
    
            System.out.println("number: " + number);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    1.2 原因

    字节码指令
    • 线程拥有自己的栈内存,读数据时会从主内存中拿,写完后会将数据写回主存
    • i++在实际执行的时候,是一系列指令,一系列指令就会导致指令交错

    在这里插入图片描述

    指令交错
    • 指令交错存在于多线程之间
    • 线程上下文切换,引发不同线程内指令交错,最终导致上述操作结果不会为0

    在这里插入图片描述

    概念
    • 线程不安全: 只会存在于多个线程共享的资源
    • 临界区: 对共享资源的多线程读写操作的代码块
    • 竞态条件: 多个线程在在临界区内,由于代码的指令交错,对共享变量资源的争抢
    多线程  读    共享资源  没问题
    多线程  读写  共享资源  可能线程不安全(指令交错)
    
    • 1
    • 2

    2. Synchronized

    • 一种阻塞式解决线程安全问题的方案

    2.1 同步代码块

    基本语法
    • 对象锁,只要为同一个对象,为任意对象(除基本类型)
    • 对象锁:尽可能用final修饰,这样保证对象的引用不可变,从而确保是同一把锁
    - synchronized: 一种阻塞式的,用来解决多线程访问共享资源引发的不安全的解决方案
    - synchronized: 可在不同代码粒度进行控制
    - synchronized: 保证了《临界区代码的原子性(字节码)》,不会因为线程的上下文切换而被打断
    - synchronized: 必须保证对对同一个对象加锁(Integer.value(0))
    
    • 1
    • 2
    • 3
    • 4
    package com.nike.erick.d03;
    
    public class Demo01 {
        private static int number = 0;
    
        /*任何对象都可以,只要保证是引用类型*/
        private static Object lock = new Object();
    
        public static void main(String[] args) throws InterruptedException {
            /*粗粒度的锁
              细粒度的锁:也可以加在每个for循环中*/
            Thread firstThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (lock) {
                        for (int i = 0; i < 100000; i++) {
                            number++;
                        }
                    }
                }
            });
    
            Thread secondThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (lock) {
                        for (int i = 0; i < 100000; i++) {
                            number--;
                        }
                    }
                }
            });
    
            firstThread.start();
            secondThread.start();
    
            firstThread.join();
            secondThread.join();
    
            System.out.println("number: " + number);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    原理
    1. a线程获取锁,执行代码
    2. 其他线程这个时候要进入,无法获取锁资源,就会被block,进入  《等待队列》
       同时进入上下文切换
       
    3. a线程执行完毕后,释放锁资源。唤醒其他线程,进行cpu的争夺
    
    • 1
    • 2
    • 3
    • 4
    • 5
    面向对象改进
    package com.nike.erick.d03;
    
    public class Demo03 {
        private static Room room = new Room();
    
        public static void main(String[] args) throws InterruptedException {
    
            Thread firstThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                        room.decrement();
                    }
                }
            });
    
            Thread secondThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                        room.increment();
                    }
                }
            });
    
            firstThread.start();
            secondThread.start();
            firstThread.join();
            secondThread.join();
    
            System.out.println("result:" + room.getValue());
        }
    }
    
    class Room {
        private int counter;
    
        // 锁对象一般用当前对象
        public void increment() {
            synchronized (this) {
                counter++;
            }
        }
    
        public void decrement() {
            synchronized (this) {
                counter--;
            }
        }
    
        public int getValue() {
            synchronized (this) {
                return counter;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    2.2 同步方法

    成员方法
    • 同步成员方法和同步代码块效果一样,必须保证同步代码块的锁对象是this对象
    • 可能锁粒度不太一样
    • 同步方法的锁对象是this,即当前对象
    @Data
    class Calculator {
        private int number;
    
        public void incr() {
            synchronized (this) {
                for (int i = 0; i < 10000; i++) {
                    number++;
                }
            }
        }
        // 同步方法
        public synchronized void decr() {
            for (int i = 0; i < 10000; i++) {
                number--;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    静态成员方法
    • 锁对象:锁用的是类的字节码对象: Calculator.class
    @Data
    class Calculator {
        private static int number;
    
        public int getNumber() {
            return number;
        }
    
        public static void incr() {
            for (int i = 0; i < 10000; i++) {
                synchronized (Calculator.class) {
                    number++;
                }
            }
        }
    
        public static synchronized void decr() {
            for (int i = 0; i < 10000; i++) {
                number--;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    锁对象
    - 同步代码块,必须保证用的锁是同一个对象,但是不能为基本数据类型
    - 同步成员方法的锁对象是this
    - 同步静态成员方法的锁对象是当前类的字节码对象     .class
    - 如果多个线程对共享变量读写,但是部分线程没有加锁保护,依然线程不安全
    
    • 1
    • 2
    • 3
    • 4

    3. 线程安全场景

    3.1 成员变量/静态成员变量

    1. 没被多线程共享:        则线程安全
    2. 被多线程共享:
         2.1 如果只读,   则线程安全
         2.2 如果有读写, 则可能发生线程不安全问题
    
    • 1
    • 2
    • 3
    • 4

    3.2 局部变量

    线程安全
    # 每个线程的方法都会创建单独的栈内存,局部变量保存在自己当前方法的栈桢内
    # 局部变量线程私有
    
    1. 局部变量是基础数据类型时: 是线程安全的
    2. 但局部变量是应用类型时:   可能不安全
      2.1 如果该对象没有逃离方法的作用访问,则线程安全
      2.2 如果该对象逃离方法的作用范围,则可能线程不安全 《引用逃离》
     
    # 避免线程安全类变为不安全类: 不要让一个类的方法被重写
    - final修饰禁止继承,或对可能引起安全的方法加private
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    引用逃离
    • 如果一个类不是final类,那么就可能被继承
    • 被继承的时候发生方法覆盖,覆盖方法如果创建新的线程,就可能发生局部变量不安全
    // 安全
    package com.nike.erick.d01;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class Demo05 {
        public static void main(String[] args) {
            ArrayList<String> list = new ArrayList<>();
            SafeCounter safeCounter = new SafeCounter();
            safeCounter.operation(list);
            System.out.println(list);
        }
    }
    
    class SafeCounter {
        public void operation(List<String> list) {
            for (int i = 0; i < 10000; i++) {
                addElement(list);
                deleteElement(list);
            }
        }
    
        public void addElement(List<String> list) {
            list.add("HELLO");
        }
    
        public void deleteElement(List<String> list) {
            list.remove(0); // 移除元素
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    // 不安全
    package com.nike.erick.d01;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class Demo05 {
        public static void main(String[] args) {
            ArrayList<String> list = new ArrayList<>();
            UnsafeCounter unsafeCounter = new UnsafeCounter();
            unsafeCounter.operation(list);
            System.out.println(list);
        }
    }
    
    class SafeCounter {
        public void operation(List<String> list) {
            for (int i = 0; i < 10000; i++) {
                addElement(list);
                deleteElement(list);
            }
        }
    
        public void addElement(List<String> list) {
            list.add("HELLO");
        }
    
        public void deleteElement(List<String> list) {
            list.remove(0); // 移除元素
        }
    }
    
    class UnsafeCounter extends SafeCounter {
        @Override
        public void deleteElement(List<String> list) {
            /*开启了新的线程来改变*/
            // index out of bound
            // 相当于把删除的操作延迟或提前了
            new Thread(() -> list.remove(0)).start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    3.3 线程安全类

    JDK类
    • 多个线程同时调用他们同一个实例的方法时,线程安全
    • 线程安全类中的方法的组合,不一定线程安全
    - String
    - Integer
    - StringBuffer
    - Random
    - Vector
    - Hashtable
    - java.util.concurrent包下的类
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    package com.nike.erick.d03;
    
    import java.util.Hashtable;
    import java.util.concurrent.TimeUnit;
    
    public class Demo04 {
    
        /*共享资源*/
        private static Hashtable<String, String> hashtable = new Hashtable<>();
    
        public static void main(String[] args) throws InterruptedException {
    
            Thread firstThread = new Thread(() -> combinedMethod(hashtable));
    
            Thread secondThread = new Thread(() -> combinedMethod(hashtable));
    
            firstThread.start();
            secondThread.start();
            firstThread.join();
            secondThread.join();
            System.out.println(hashtable.size());
        }
    
        // 方法的组合不是 线程安全的
        // 如果要线程安全,必须将组合方法也设置成 synchronized
        public static void combinedMethod(Hashtable<String, String> hashtable) {
            if (null == hashtable.get("name")) {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                hashtable.put("name", "erick");
                System.out.println(Thread.currentThread().getName());
                hashtable.put(Thread.currentThread().getName(), "00");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    不可变类
    • 类中属性都是final,不能修改
    • 如 String,Integer
    #  实现线程安全类的问题
    - 无共享变量
    - 共享变量不可变
    - synchronized互斥修饰
    
    • 1
    • 2
    • 3
    • 4
  • 相关阅读:
    测试:设计测试用例
    Python 编程基础 | 第二章-基础语法 | 2.1、缩进规则
    【零散技术】妙用Odoo开发者模式下的数据模型
    vue3 和 vue2 区别,面试简答
    前端入门到入土?
    双端队列(Deque)
    图形库篇 | EasyX | 基本介绍
    【C++】STL简介 | STL六大组件 | string类 | string类对象操作
    华为自研编程语言仓颉首次面世,首席架构师冯新宇确认出席2024全球软件研发技术大会!
    Spark on Yarn With K8s
  • 原文地址:https://blog.csdn.net/weixin_43374578/article/details/127945629