• Java 多线程基础


    1. 认识线程

    1.1 概念

    1.1.1 线程是什么

    线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
    一个线程就是一个"执行流", 每个线程可以按照顺序执行自己的代码, 多个线程之间"同时"执行着多份代码

    1.1.2 为什么要有线程

    • 单核CPU的发展到了瓶颈, 要再想提高计算机算力的话, 就需要多核CPU, 而并发编程便能充分利用多核CPU资源
    • 有些任务场景需要"等待IO"为了让"等待IO"的时间能去做其他的事情, 也需要用到并发编程

    虽然多进程也能实现并发编程, 但是多线程要比多进程更轻量

    • 创建线程比创建进程更快
    • 销毁线程比销毁进程更快
    • 调度线程比调度进程更快

    1.1.3 进程和线程的区别

    • 进程是包含线程的, 每个进程必须包含一个线程, 就是主线程
    • 进程和进程之间是不共享内存空间的, 同一个进程的线程之间共享内存空间
    • 进程是系统分配资源的最小单位, 线程是系统调度的最小单位

    1.1.4 Java的线程和操作系统线程的关系

    线程是操作系统中的概念, 操作系统内核实现了这样的机制,并且对用户层提供了一些 API 供用户使
    用(例如 Linux 的 pthread 库).
    Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装

    1.2 第一个多线程程序

    下面我们写一个简单的多线程程序, 先不关心它是怎么写的, 就先简单的了解一下多线程,
    感受多线程程序和单线程程序的区别

    • 每个线程是一个独立的执行流
    • 多个线程之间是"并发"执行的
    package Thread;
    
    import java.util.Random;
    
    public class ThreadDemo1 {
        private static class MyThread extends Thread {
            @Override
            public void run() {
                Random random = new Random();
                while (true) {
                    // 打印线程的名称
                    System.out.println(Thread.currentThread().getName());
                    try {
                    // 随机停止运行 0 ~ 9 秒
                        Thread.sleep(random.nextInt(10));
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    
    
        public static void main(String[] args) {
    
            MyThread thread1 = new MyThread();
            MyThread thread2 = new MyThread();
            MyThread thread3 = new MyThread();
            // 启动线程
            thread1.start();
            thread2.start();
            thread3.start();
    
            Random random = new Random();
            while (true) {
                System.out.println(Thread.currentThread().getName());
                try {
                    // 随机停止运行 0 ~ 9 秒
                    Thread.sleep(random.nextInt(10));
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
    
    • 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

    在这里插入图片描述
    下面我们用jconsole观察线程
    首先我们打开jconsole
    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    1.3 创建线程

    1. 继承Thread 类来创建一个线程
    public class ThreadDemo2 {
        // 1.继承一个Thread 类来创建一个线程类
        class MyThread extends Thread {
            @Override
            // 重写里面的run方法
            public void run() {
                System.out.println("这里是线程运行的代码");
            }
        }
    
        public static void main(String[] args) {
            // 2.创建一个 MyThread 实例
            MyThread thread1 = new MyThread();
            // 3. 调用 start 方法启动线程
            thread1.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    1. 实现 Runnable 接口
    public class ThreadDemo3 {
           // 1. 实现Runnable接口
           static class MyRunnable implements Runnable {
            @Override
            public void run() {
                System.out.println("这里是线程执行的代码");
            }
        }
        public static void main(String[] args) {
            // 2. 创建Thread类实例, 调用Thread类的构造方法时将Runnable对象作为target 参数
            Thread thread1 = new Thread(new MyRunnable());
            // 启动线程
            thread1.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这里我们要注意的是:
    实现Runnable接口,并不能直接启动或者说实现一个线程,Runnable接口和线程是两个不同的概念
    换句话说,一个类,实现Runnable接口,这个类可以做很多事情,不仅仅只被用于线程,也可以用于其他功能!

    • 继承 Thread 类直接使用 this 就表示当前线程对象的引用
    • 而实现 Runnable 接口, this 表示的是 MyRunnable 的引用, 需要使用 Thread.currentThread() 表示当前线程对象的引用
    1. 使用匿名内部类创建 Thread 子类对象
    public class ThreadDemo5 {
        public static void main(String[] args) {
            Thread thread1 = new Thread() {
                @Override
                public void run() {
                    System.out.println("使用匿名内部类创建 Thread 子类对象");
                }
            };
            //启动线程
            thread1.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    1. 匿名内部类创建 Runnable 子类对象
    public class ThreadDemo6 {
        public static void main(String[] args) {
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("使用匿名内部类创建 Runnable 子类对象");
                }
            });
            // 启动线程
            thread1.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    1. lambda 表达式创建 Runnable 子类对象
    public class ThreadDemo7 {
        public static void main(String[] args) {
            Thread thread1 = new Thread(() -> {
                System.out.println("使用匿名内部类创建 Thread 对象");
            });
        // 启动线程
        thread1.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    注:
    Thread 类中run()和start()方法的区别

    作用功能不同:
    run方法的作用是描述线程具体要执行的任务;
    start方法的作用是真正的去申请系统线程
    运行结果不同:
    run方法是一个类中的普通方法,主动调用和调用普通方法一样,会顺序执行一次;
    start调用方法后, start方法内部会调用Java 本地方法(封装了对系统底层的调用)真正的启动线程,并执行run方法中的代码,run 方法执行完成后线程进入销毁阶段。

    1.4 多线程的优势

    多线程的主要优势之一就是增加运行速度

    下面我们举个例子具体看
    我们使用并发和串行方式计算 a 和 b 的值, 分别让 a 和 b 自加20_0000_0000次,

    /**
     * @describe
     * @author chenhongfei
     * @version 1.0
     * @date 2023/9/23
     */
    package Thread;
    
    import java.util.Random;
    import java.util.concurrent.CountDownLatch;
    
    public class ThreadDemo8 {
        private static final long count = 20_0000_0000;
        public static void main(String[] args) throws InterruptedException {
            // 使用并行方式
            concurrency();
            // 使用串行方式
            serial();
        }
    
        // 并行
        private static void concurrency() throws InterruptedException {
            long begin = System.nanoTime();
            // 利用一个线程计算 a 的值
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    int a = 0;
                    for (int i = 0; i < count; i++) {
                        a++;
                    }
                }
            });
            //启动线程
             thread1.start();
             // 主线程内计算b的值
            int b = 0;
            for (int i = 0; i < count; i++) {
                b++;
            }
            // 等待Thread线程结束
            thread1.join();
            long end = System.nanoTime();
            double ms =  (end - begin) * 1.0 / 1000 /1000; //1 纳秒=1e-6 毫秒
            System.out.printf("并发: %f 毫秒%n",ms);
    
        }
    
        // 串行
        private static void serial() {
            // 全在主线程内计算a, b 的值
            long begin = System.nanoTime();
            int a = 0;
            int b = 0;
            for (int i = 0; i < count; i++) {
                a++;
            }
            for (int i = 0; i < count; i++) {
                b++;
            }
            long end = System.nanoTime();
            double ms =  (end - begin) * 1.0 / 1000 /1000; //1 纳秒=1e-6 毫秒
            System.out.printf("串行: %f 毫秒%n",ms);
        }
    }
    
    • 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
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    在这里插入图片描述

    2. Thread 类及其常用的方法

    Thread类是Java中用于创建线程的类。线程是程序执行的最小单元,它允许多个任务并发执行,提高了程序的效率。Thread类的实例表示一个独立的执行线程,可以通过继承Thread类或实现Runnable接口来创建线程。Thread类提供了一系列方法来管理线程的状态和行为,比如启动线程、暂停线程、恢复线程、等待线程完成等。在Java中,线程的使用非常普遍,比如在网络编程、多线程服务器、GUI应用程序等领域都会用到线程。

    2.1 Thread 的常见构造方法

    方法说明
    Thread()创建线程对象
    Thread(Runnable target)使用 Runnable 对象创建线程对象
    Thread(String name)创建线程对象,并命名
    Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名
    Thread t1 = new Thread();
    Thread t2 = new Thread(new MyRunnable());
    Thread t3 = new Thread("这是我的名字");
    Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
    
    • 1
    • 2
    • 3
    • 4

    2.2 Thread 的几个常见属性

    属性获取方法
    IDgetId()
    名称getName()
    状态getState()
    优先级getPriorty()
    是否后台线程isDaemom()
    是否存活isAlive()
    是否被中断isInterrupt()
    • ID 是线程的唯一标识,不同线程不会重复
    • 名称是各种调试工具用到
    • 状态表示线程当前所处的一个情况
    • 优先级高的线程理论上来说更容易被调度到
    • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
    • 是否存活,即简单的理解,为 run 方法是否运行结束了

    具体情况我们参考下面代码

    /**
     * @describe
     * @author chenhongfei
     * @version 1.0
     * @date 2023/9/23
     */
    package Thread;
    
    public class ThreadDemo9 {
        public static void main(String[] args) {
            Thread thread1 = new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + "我还活着");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println(Thread.currentThread().getName() + "我即将死去");
            });
    
            System.out.println(Thread.currentThread().getName() + ": ID" + thread1.getId());
            System.out.println(Thread.currentThread().getName() + ": 名称" + thread1.getName());
            System.out.println(Thread.currentThread().getName() + ": 状态" + thread1.getState());
            System.out.println(Thread.currentThread().getName() + ": 优先级" + thread1.getPriority());
            System.out.println(Thread.currentThread().getName() + ": 后台线程" + thread1.isDaemon());
            System.out.println(Thread.currentThread().getName() + ": 活着" + thread1.isAlive());
            System.out.println(Thread.currentThread().getName() + ": 被中断" + thread1.isInterrupted());
    
            //启动线程
            thread1.start();
    
            while (thread1.isAlive()) {
                System.out.println(Thread.currentThread().getName() + ": 状态" + thread1.getState());
            }
        }
    }
    
    • 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

    2.3 启动一个线程 - start()

    在Java中,start()方法通常用于启动线程。它是定义在Thread类中的方法,用于启动线程。
    当调用start()方法时,线程会从它的run()方法开始执行。run()方法是线程的执行体,它包含了线程要执行的代码。
    当线程启动后,它将从run()方法开始执行,直到该方法结束或线程被中止。在执行期间,线程可以访问共享变量和对象,并且可以与其他线程并发执行。

    • 调用 start() 方法, 才真的在操作系统的底层创建出一个线程.
    • 重写run() 方法是给线程提供具体的指令清单
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            // 线程执行的代码(重写run 方法)
            System.out.println("Thread started.");
        }
    });
    thread.start(); // 启动线程
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在上面的示例中,我们创建了一个新的线程对象,将一个实现了Runnable接口的对象传递给了构造函数。在run()方法中,我们打印了一条消息表示线程已经启动。然后,我们调用start()方法来启动线程。
    需要注意的是,start()方法并不会阻塞调用它的线程。启动线程后,程序会继续执行其他代码。如果需要等待线程执行结束后再继续执行当前线程,可以使用join()方法。

    2.4 中断一个线程

    一个线程一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那该如何通知这个线程停止呢?这就涉及到我们的停止线程的方式了。
    下面我们介绍两种方式:

    • 1.通过共享的标记来进行沟通

    通过设置一个标志位,线程在执行时可以检查这个标志位,如果发现标志位被设置了,那么线程就认为自己被中断了。

    • 2.调用interrupt() 方法来通知

    下面是一个简单的使用标志位中断线程的实例:

    public class ThreadDemo10 {
        private static volatile boolean isInterrupted = false;  //使用volatile 修饰, 保证线程间的可见性
        static Thread thread1 = new Thread(() -> {
            while (!isInterrupted) {
                System.out.println(Thread.currentThread().getName() + "开始转账");
                try {
                    Thread.sleep(1000);  // 模拟任务耗时
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(Thread.currentThread().getName() + "取消转账, 差点误了大事");
        });
    
        public static void main(String[] args) {
            thread1.start();    // 线程启动, 开始转账
            isInterrupted = true;   // 取消转账
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    我们也可以使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位.

    Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记

    下面先介绍几个方法

    方法说明
    public void interrupt()中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位
    public static booleaninterrupted()判断当前线程的中断标志位是否设置,调用后清除标志位
    public booleanisInterrupted()判断对象关联的线程的标志位是否设置,调用后不清除标志位
    public class ThreadDemo11 {
        static Thread thread1 = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {    // Thread.currentThread().isInterrupted() 这就是标志位
                System.out.println(Thread.currentThread().getName() + "开始转账");
                try {
                    Thread.sleep(1000);  // 模拟任务耗时
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(Thread.currentThread().getName() + "取消转账, 差点误了大事");
        });
    
        public static void main(String[] args) {
            thread1.start();    // 线程启动, 开始转账
            thread1.interrupt();   //设置标志位   取消转账
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    线程收到通知的方法有两种:

    1. 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,清除中断标志

    当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程.

    1. 否则, 只是内部的一个中断标志被设置,thread 可以通过:
      Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
      Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志

    标志位就相当于一个开关
    Thread.isInterrupted() 相当于按下开关, 开关自动弹起来了, 这个称为"清楚标志位"
    Thread.currentThread().isInterrupted() 相当于按下开关, 开关不弹起来, 这个称为"不清除标志位"

    • 使用Thread.isInterrupted() 线程中断, 会清除标志位
    public class ThreadDemo12 {
        public static void main(String[] args) {
            Thread thread1 = new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.interrupted());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            thread1.start();
            thread1.interrupt();   // 通知中断线程, 设置标志位
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    只有第一个是 true , 后面都是 false 因为"开关弹回去了"
    在这里插入图片描述

    • 使用Thread.currentThread().Interrupted() 线程中断, 标志位不会清除
    
    public class ThreadDemo13 {
        public static void main(String[] args) {
            Thread thread = new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().isInterrupted());
                }
            });
            thread.start();
            thread.interrupt();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    全部都是 true , 因为标志位不会清除
    在这里插入图片描述

    2.5 等待一个线程 - join()

    有时候, 我们经常需要等待一个线程完成它的工作后, 才能进行下一步工作, 比如说, 这个月发工资我要买个手机, 只有等到了工资到账, 才能去买手机, 这时候我们就需要明确等待线程的结束.

    每个Thread实例都有一个join()方法,该方法使得当前正在执行的线程暂停执行(阻塞),直到被join的线程执行结束(即从run()方法返回)。

    public class ThreadDemo14 {
        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(() -> {
                for (int i = 0; i < 5; i++) {
                    System.out.println("还有" + (5-i) +"天发工资");
                }
                System.out.println("工资已到账");
            });
    
            Thread thread2 = new Thread(() -> {
                System.out.println("正在去买手机的路上");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("买到了手机");
            });
    
            thread1.start();
            thread1.join();   //直到 thread1 执行结束才执行后面内容
            thread2.start();
            thread2.join();
            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

    在这里插入图片描述
    在这个例子中,主线程(Main Thread)会等待thread1线程执行结束,然后再执行自己的后续操作(让thread2执行)。join()方法的使用使得主线程能够同步地执行与子线程的结束。

    如果把两个join() 注释掉, 效果如下:
    在这里插入图片描述
    此外还可以使用wait(), notify() 和 notifyAll(), 这个后面我们细说.

    2.6 获取当前线程引用

    方法说明
    public static Thread currentThread();返回当前线程对象的引用
    public class ThreadDemo {
      public static void main(String[] args) {
        Thread thread = Thread.currentThread();
     }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    currentThread() 现在就是指向当前执行线程的一个引用。你可以用这个引用来获取当前线程的各种信息,例如它的名称、它的优先级、它是否是守护线程等等。

    System.out.println("Current thread name: " + currentThread.getName());
    System.out.println("Current thread priority: " + currentThread.getPriority());
    System.out.println("Is current thread a daemon?: " + currentThread.isDaemon());
    
    • 1
    • 2
    • 3

    2.7 休眠当前线程

    我们可以使用Thread.sleep()方法来使当前线程休眠(暂时停止执行)一段时间。这个方法接受一个以毫秒为单位的时间参数,指定线程应该休眠的时间。

    • 因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。
    public class ThreadDemo15 {
        public static void main(String[] args) {
            Thread thread1 = new Thread(() -> {
                System.out.println(System.currentTimeMillis());
                try {
                    Thread.sleep(3 * 1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(System.currentTimeMillis());
            });
            thread1.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
  • 相关阅读:
    java:aocache 与Spring Aop兼容问题
    LeetCode 630. Course Schedule III【反悔贪心,堆,排序】中等
    C和指针 第15章 输入/输出函数 15.9 未格式化的行I/O
    时间轴_3D打印
    基于JavaGUI的简易图书管理系统
    汽车soa架构介绍
    leecode#只出现一次数字#环形链表
    使用微信小程序编写会议OA项目-首页
    ctf:kali工具: msfvenom
    (计算机组成原理)第三章存储系统-第四节2:固态硬盘SSD
  • 原文地址:https://blog.csdn.net/qq_43339789/article/details/133188070