• Java进阶(十二)多线程


    十二、多线程

    什么是线程?

    • 线程(thread)是一个程序内部的一条执行路径。

    • 我们之前启动程序执行后,main方法的执行其实就是一条单独的执行路径。

      public static void main(String[] args) {
      	// 执行代码
      }
      
      • 1
      • 2
      • 3
    • 程序中如果只有一条执行路径,那么这个程序就是单线程的程序。

    什么是多线程?

    • 多现场是指从软硬上实现多条执行流程的技术。

    多线程用在哪里,有什么好处?

    • 例如:消息通信、淘宝、京东系统。

    关于多线程需要学会什么?

    • 多线程的创建:如何在程序中实现多线程,有哪些方式,各自有什么优缺点。
    • Thread类的常用方法:线程的代表是Thread类,Thread提供了哪些线程的操作给我们?
    • 线程安全、线程同步:多个线程同时访问一个共享的数据的时候会出现问题,如何去解决?
    • 线程通信、线程池:线程与线程间需要配合完成一些事情。线程池是一种线程优化方案,可以用一种更好的方式使用多线程。
    • 定时器、线程状态等:如何在程序中实现定时器?线程在执行的过程中会有很多不同的状态,理解这些状态有助于理解线程的执行原理。

    1.多线程的创建

    a.方式一:继承Thread类

         class PrimeThread extends Thread {
             long minPrime;
             PrimeThread(long minPrime) {
                 this.minPrime = minPrime;
             }
    
             public void run() {
                 // compute primes larger than minPrime
                  . . .
             }
         }
    
         PrimeThread p = new PrimeThread(143);
         p.start();
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    Thread类:

    • Java是通过java.lang.Thread类来代表线程的。
    • 按照面向对象的思想,Thread类应该提供了实现多线程的方式。

    多线程的实现方案一:继承Thread类

    1. 定义一个子类MyThread继承线程类java.lang.Thread。
    2. 重写run()方法。
    3. 创建MyThread类的对象。
    4. 调用线程对象的start()方法启动线程(启动后还是执行run方法的)

    Test.java

    /**
     * 目标:多线程的创建方式一:继承Thread类实现
     */
    public class Test {
        public static void main(String[] args) {
            // 3.new一个新线程对象
            Thread thread = new MyThread();
    
            // 4.调用start方法启动线程(执行的还是run方法)
            thread.start();
    
            for (int i = 0; i < 10; i++) {
                System.out.println("主线程执行输出:" + i);
            }
    
        }
    }
    
    /**
     * 1.自定义线程类
     */
    class MyThread extends Thread {
    
        /**
         * 2.重写run方法 里面是定义线程之后做什么
         */
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("子线程执行输出:" + i);
            }
        }
    }
    
    • 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
    子线程执行输出:0
    主线程执行输出:0
    主线程执行输出:1
    主线程执行输出:2
    主线程执行输出:3
    子线程执行输出:1
    主线程执行输出:4
    主线程执行输出:5
    主线程执行输出:6
    主线程执行输出:7
    主线程执行输出:8
    主线程执行输出:9
    子线程执行输出:2
    子线程执行输出:3
    子线程执行输出:4
    子线程执行输出:5
    子线程执行输出:6
    子线程执行输出:7
    子线程执行输出:8
    子线程执行输出:9
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    方式一优点和缺点:

    • 优点:编码简单。
    • 缺点:线程类已经继承Thread,无法继承其他类,不利于扩展。

    总结:

    1. 为什么不直接调用run()方法,而是调用start()启动线程。
      • 直接调用run()方法会当成普通方法执行,此时相当于还是单线程执行。
    2. 不要把主线程任务放在子线程之前。
      • 这样主线程一直是先跑完,相当于是一个单线程的效果。

    b.方式二:实现Runnable接口

         class PrimeRun implements Runnable {
             long minPrime;
             PrimeRun(long minPrime) {
                 this.minPrime = minPrime;
             }
    
             public void run() {
                 // compute primes larger than minPrime
                  . . .
             }
         }
    
         PrimeRun p = new PrimeRun(143);
         new Thread(p).start();
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    实现Runnable接口:

    • 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法。
    • 创建MyRunnable任务对象。
    • 把MyRunnable任务对象交给Thread处理。
    • 调用线程对象的start()方法启动线程。

    Thread构造器:

    构造器说明
    public Thread(String name)可以为当前线程指定名称。
    public Thread(Runnable target)封装Runnable对象称为线程对象。
    public Thread(Runnable target, String name)封装Runnable对象成为线程对象,并指定线程名称。

    Test.java

    /**
     * 目标:学会线程的创建方式二 理解它的优缺点
     */
    public class Test {
        public static void main(String[] args) {
            // 3.创建一个任务对象
            Runnable runnable = new MyRunnable();
    
            // 4.把任务对象交给Thread处理
            Thread thread = new Thread(runnable);
    
            // 5.启动线程
            thread.start();
    
            for (int i = 0; i < 10; i++) {
                System.out.println("主线程执行输出:" + i);
            }
        }
    }
    
    /**
     * 1.定义一个线程任务类 实现Runnable接口
     */
    class MyRunnable implements Runnable {
        /**
         * 2.重写run方法 定义线程的执行任务
         */
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("子线程执行输出:" + i);
            }
        }
    }
    
    
    • 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
    主线程执行输出:0
    子线程执行输出:0
    主线程执行输出:1
    主线程执行输出:2
    主线程执行输出:3
    主线程执行输出:4
    主线程执行输出:5
    主线程执行输出:6
    主线程执行输出:7
    主线程执行输出:8
    主线程执行输出:9
    子线程执行输出:1
    子线程执行输出:2
    子线程执行输出:3
    子线程执行输出:4
    子线程执行输出:5
    子线程执行输出:6
    子线程执行输出:7
    子线程执行输出:8
    子线程执行输出:9
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    方式二优点和缺点:

    • 优先:线程任务类只是实现接口,可以继续 继承类和实现接口,扩展性强。
    • 缺点:编程多一层对象包装,如果线程池有执行结果是不可以直接返回的。

    实现Runnable接口:匿名内部类形式

    1. 创建Runnable的匿名内部类对象。
    2. 交给Thread处理。
    3. 调用线程对象的start()启动线程。

    Test.java

    /**
     * 目标:学会线程的创建方式二 匿名内部类的方式实现 语法形式
     */
    public class Test {
        public static void main(String[] args) {
            // 1.创建Runnable的匿名内部类对象
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        System.out.println("子线程1执行输出:" + i);
                    }
                }
            };
    
            // 2.交给线程对象
            Thread thread = new Thread(runnable);
    
            // 3.启动线程
            thread.start();
    
            // 简化版一
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        System.out.println("子线程简化版一执行输出:" + i);
                    }
                }
            }).start();
    
            // 简化版二
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    System.out.println("子线程简化版二执行输出:" + i);
                }
            }).start();
    
            for (int i = 0; i < 10; i++) {
                System.out.println("主线程执行输出:" + i);
            }
        }
    }
    
    
    • 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

    c.方式三:JDK5.0新增,实现Callable接口

    前两种线程创建方式都存在的问题:

    • 重写的run方法均不能直接返回线程执行的结果。
    • 不适合需要返回线程执行结果的业务场景。

    解决方案:

    • JDK5.0提供了Callable和FutureTask来实现。
    • 这种方式的优点是:可以得到线程执行的结果。

    利用Callable、FutureTask接口实现:

    1. 得到任务对象
      • 定义类实现Callable接口、重写call()方法、封装要做的事情。
      • 用FutureTask把Callable对象封装成线程任务对象。
    2. 把线程任务对象交给Thread处理。
    3. 调用Thread的start()方法启动线程,执行任务。
    4. 线程执行完毕后、通过FutureTask的get()方法去获取任务执行的结果。

    FutureTask的API:

    方法名称说明
    public FutureTask<>(Callable call)把Callable对象封装成FutureTask对象。
    public V get() throws Exception获取线程执行call()方法返回的结果。

    Test.java

    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    
    /**
     * 目标:学会线程的创建方式三 实现Callable接口、结合FutureTask完成
     */
    public class Test {
        public static void main(String[] args) {
            // 3.创建Callable任务对象
            Callable<String> callable = new MyCallable(100);
    
            // 4.用FutureTask把Callable对象封装成线程任务对象
            // FutureTask实现了Runnable接口
            // FutureTask可以在线程执行完毕之后通过调用其get()方法得到线程执行完成的结果
            FutureTask<String> futureTask = new FutureTask<>(callable);
    
            // 5.把线程任务对象交给Thread处理
            Thread thread = new Thread(futureTask);
    
            // 6.启动线程
            thread.start();
    
            // 再创建一个线程
            Callable<String> callable1 = new MyCallable(200);
            FutureTask<String> futureTask1 = new FutureTask<>(callable1);
            Thread thread1 = new Thread(futureTask1);
            thread1.start();
    
            // 7.获取返回结果
            try {
                // 会等待子线程执行完毕后 才会提取结果
                String result = futureTask.get();
                System.out.println("第一个线程执行的结果:" + result);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
    
            try {
                String result1 = futureTask1.get();
                System.out.println("第二个线程执行的结果:" + result1);
    
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
    
        }
    }
    
    /**
     * 1.定义一个任务类 实现Callable借口 应该申明线程任务执行完毕后的结果的数据类型
     */
    class MyCallable implements Callable<String> {
    
        private int number;
    
        public MyCallable(int number) {
            this.number = number;
        }
    
    
        /**
         * 2.重写call方法
         *
         * @return 
         * @throws Exception
         */
        @Override
        public String call() throws Exception {
            int sum = 0;
    
            for (int i = 0; i <= number; i++) {
                sum += i;
            }
    
            return "子线程执行的结果是:" + sum;
        }
    }
    
    
    • 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
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79

    方式三优点和缺点:

    • 优点:线程任务类只是实现接口,可以继续 继承类和实现接口,扩展性强。
    • 优点:可以在线程执行完毕后去获取线程执行的结果。
    • 缺点:编写代码复杂一点。

    d.总结

    方式优点缺点
    继承Thread类编程比较简单,可以直接使用Thread类中的方法。扩展性较差,不能再继承其他类,不能返回线程执行的结果。
    实现Runnable接口扩展性强,实现该接口的同时还可以继承其他的类。编程相对复杂,不能返回线程执行的结果。
    实现Callable接口扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果。编程相对复杂。

    2.Thread的常用方法

    Thread常用API说明:

    • Thread常用方法:获取线程名称getName()、设置名称setName()、获取当前线程对象currentThread()。
    • 至于Thread类提供的诸如:yield、join、interrput、不推荐的方法stop、守护线程、线程优先级等线程的控制方法,在开发中很少使用。

    当有很多线程在执行的时候,我们要怎么去区分这些线程?

    • 此时需要使用Thread的常用方法:getName()、setName()、currentThread()等。

    Thread获取和设置线程名称:

    方法名称说明
    String getName()获取当前线程的名称,默认线程名称是Thread-索引。
    void setName(String name)将此线程的名称更改为指定的名称,通过构造器也可以设置线程名称。

    Test.java

    /**
     * 目标:线程的API
     */
    public class Test {
        /**
         * main方法主要是由主线程负责调度
         */
        public static void main(String[] args) {
    
            // 创建MyThread对象
            Thread thread = new MyThread();
            // 设置线程名称
            thread.setName("1号");
            // 启动子线程
            thread.start();
    
            Thread thread1 = new MyThread();
            thread.setName("2号");
            thread1.start();
    
            // 获取当前线程对象
            // 主线程名称叫做main
            Thread threadMain = Thread.currentThread();
    
            for (int i = 0; i < 10; i++) {
                System.out.println(threadMain.getName() + "输出:" + i);
            }
        }
    }
    
    /**
     * 自定义线程对象
     */
    class MyThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "输出:" + i);
            }
        }
    }
    
    
    • 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

    Thread的构造器:

    方法名称说明
    public Thread(String name)可以为当前线程指定名称。
    public Thread(Runnable target)封装Runnable对象成为线程对象。
    public Thread(Runnable target, String name)封装Runable对象成为线程对象,并指定线程名称。

    Test.java

    /**
     * 目标:线程的API
     */
    public class Test {
        /**
         * main方法主要是由主线程负责调度
         */
        public static void main(String[] args) {
    
            // 创建MyThread对象
            Thread thread = new MyThread("1号");
            // 启动子线程
            thread.start();
    
            Thread thread1 = new MyThread("2号");
            thread1.start();
    
            // 获取当前线程对象
            // 主线程名称叫做main
            Thread threadMain = Thread.currentThread();
            // threadMain.setName("主线程");
    
            for (int i = 0; i < 10; i++) {
                System.out.println(threadMain.getName() + "输出:" + i);
            }
        }
    }
    
    /**
     * 自定义线程对象
     */
    class MyThread extends Thread {
        public MyThread() {
        }
    
        public MyThread(String name) {
            // 为当前线程对象设置名称 传送至父类的有参数构造器初始化名称
            super(name);
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "输出:" + i);
            }
        }
    }
    
    • 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

    Thread类的线程休眠方法:

    方法名称说明
    public static void sleep(long time)让当前线程休眠指定的时间后再继续执行,单位为毫秒。

    Test.java

    public class Test {
        public static void main(String[] args) throws Exception {
            for (int i = 0; i <= 5; i++) {
                System.out.println("输出:" + i);
    
                if (i == 3) {
                    // 休眠3秒
                    Thread.sleep(3000);
                }
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    总结:

    1. Thread常用方法、构造器。

      方法名称说明
      String getName()获取当前线程的名称,默认线程名称是Thread-索引。
      void setName(String name)设置线程名称。
      public static Thread currentThread()返回对当前正在执行的线程对象的引用。
      public static void sleep(long time)让线程休眠指定的时间,单位为毫秒。
      public void run()线程任务方法。
      public void start()线程启动方法。
    2. Thread常用方法、构造器。

      构造器说明
      public Thread(String name)可以为当前线程指定名称。
      public Thread(Runable target)把Runnable对象交给线程对象。
      public Thread(Runnable target, String name)把Runnable对象交给线程对象,并指定线程名称。

    3.线程安全

    a.线程安全问题是什么、发生的原因

    线程安全问题:

    • 多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。

    取钱模型演示:

    • 需要:小明和小红是一对夫妻,他们有一个共同的账号,余额是10万元。
    • 如果小明和小红同时来取钱,而且两人都要取钱10万元,可能两个同时取钱成功,余额为负数。

    总结:

    1. 线程安全问题出现的原因?
      • 存在多线程并发。
      • 同时访问共享资源。
      • 存修改共享资源。

    b.线程安全问题案例模拟

    需求:

    • 小明和小红是一对夫妻,他们有一个共同的账号,余额是10万元,模拟两个同时去取钱10万。

    分析:

    • 需要提供一个账户类,创建一个账户对象代表两个的共享账户。
    • 需要定义一个线程类,线程类可以处理账户对象。
    • 创建两个线程,传入同一个账户对象。
    • 启动两个线程,去同一个账户对象中取钱10万。

    Account.java

    public class Account {
        private String cardId;
        private double money;
    
        public String getCardId() {
            return cardId;
        }
    
        public void setCardId(String cardId) {
            this.cardId = cardId;
        }
    
        public double getMoney() {
            return money;
        }
    
        public void setMoney(double money) {
            this.money = money;
        }
    
        public Account() {
        }
    
        public Account(String cardId, double money) {
            this.cardId = cardId;
            this.money = money;
        }
    
        /**
         * 取钱
         * @param money 取出金额
         */
        public void drawMoney(double money) {
            // 查看是谁来取钱
            String name = Thread.currentThread().getName();
    
            // 1.判断金额是否足够
            if (this.money >= money) {
                // 2.取钱
                System.out.println(name + "取钱成功,取出:" + money);
    
                // 3.更新余额
                // 此处将更新余额操作放在第三步而不是第二步 是为了模拟出现线程安全的场景
                this.money = this.money - money;
    
                System.out.println(name + "取钱后剩余:" + this.money);
            } else {
                System.out.println(name + "来取钱,余额不足!");
            }
        }
    }
    
    
    • 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

    DrawThread.java

    /**
     * 取钱线程类
     */
    public class DrawThread extends Thread {
        // 账户对象
        private final Account account;
    
        public DrawThread(Account account, String threadName) {
            // 线程名称
            super(threadName);
            this.account = account;
        }
    
        @Override
        public void run() {
            // 取钱
            account.drawMoney(100000);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    Test.java

    public class Test {
        public static void main(String[] args) {
            // 1.定义线程类 创建一个共享账户对象
            Account account = new Account("BOC-7979", 100000);
    
            // 2.创建2个线程对象
            new DrawThread(account, "小明").start();
            new DrawThread(account, "小红").start();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    小红取钱成功,取出:100000.0
    小红取钱后剩余:0.0
    小明取钱成功,取出:100000.0
    小明取钱后剩余:-100000.0
    
    • 1
    • 2
    • 3
    • 4

    总结:

    1. 线程安全问题发生的原因是什么?
      • 多个线程同时访问同一个共享资源且存在修改该资源。

    4.线程同步

    a.同步思想概述

    线程同步:

    • 为了解决线程安全问题。

    线程同步的核心思想:

    • 加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕之后解锁,然后其他线程才能使用。

    b.方式一:同步代码块

    同步代码块:

    • 作用:把出现线程安全问题的核心代码上锁。
    • 原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。
    synchronized (同步锁对象) {
    	// 操作共享资源的代码(核心代码)
    }
    
    • 1
    • 2
    • 3

    锁对象要求:

    • 理论上:锁对象只要对于当前同时执行的线程来说是同一个对象即可。

    锁对象用任意唯一的对象好不好?

    • 不好,会影响其他无关线程的执行。

    锁对象的规范要求:

    • 规范:建议使用共享资源作为锁对象。
    • 对于实例方法建议使用this作为锁对象。
    • 对于静态方法建议使用字节码(类名.class)对象作为锁对象。

    Account.java

    public class Account {
        private String cardId;
        private double money;
    
        public String getCardId() {
            return cardId;
        }
    
        public void setCardId(String cardId) {
            this.cardId = cardId;
        }
    
        public double getMoney() {
            return money;
        }
    
        public void setMoney(double money) {
            this.money = money;
        }
    
        /**
         * 静态方法使用 类名.class 作为锁对象
         */
        public static void run() {
            synchronized (Account.class) {
    
            }
        }
    
        public Account() {
        }
    
        public Account(String cardId, double money) {
            this.cardId = cardId;
            this.money = money;
        }
    
        /**
         * 取钱
         * @param money 取出金额
         */
        public void drawMoney(double money) {
            // 查看是谁来取钱
            String name = Thread.currentThread().getName();
    
            // 同步代码快
            // 此处名字无意义 字符串为常量池中唯一对象
            // synchronized ("银行卡") {
    
            // this = account
            synchronized (this) {
                // 1.判断金额是否足够
                if (this.money >= money) {
                    // 2.取钱
                    System.out.println(name + "取钱成功,取出:" + money);
    
                    // 3.更新余额
                    // 此处将更新余额操作放在第三步而不是第二步 是为了模拟出现线程安全的场景
                    this.money = this.money - money;
    
                    System.out.println(name + "取钱后剩余:" + this.money);
                } else {
                    System.out.println(name + "来取钱,余额不足!");
                }
            }
        }
    }
    
    
    • 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
    • 66
    • 67
    • 68

    DrawThread.java

    /**
     * 取钱线程类
     */
    public class DrawThread extends Thread {
        // 账户对象
        private final Account account;
    
        public DrawThread(Account account, String threadName) {
            // 线程名称
            super(threadName);
            this.account = account;
        }
    
        @Override
        public void run() {
            // 取钱
            account.drawMoney(100000);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    Test.java

    public class Test {
        public static void main(String[] args) {
            // 1.定义线程类 创建一个共享账户对象
            Account account = new Account("BOC-7979", 100000);
    
            // 2.创建2个线程对象
            new DrawThread(account, "小明").start();
            new DrawThread(account, "小红").start();
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    小明取钱成功,取出:100000.0
    小明取钱后剩余:0.0
    小红来取钱,余额不足!
    
    • 1
    • 2
    • 3

    总结:

    1. 同步代码块是如何实现线程安全的?
      • 对出现问题的核心代码使用synchronized进行加锁。
      • 每次只能一个线程占锁进入访问。
    2. 同步代码块的同步锁对象有什么要求》
      • 对于实例方法建议使用this作为锁对象。
      • 对于静态方法使用字节码(类名.class)对象作为锁对象。

    c.方式二:同步方法

    同步方法:

    • 作用:把出现线程安全问题的核心方法上锁。
    • 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

    格式:

    修饰符 synchronized 返回值类型 方法名称(形参列表) {
    	// 操作共享资源的代码
    }
    
    • 1
    • 2
    • 3
        /**
         * 取钱
         * @param money 取出金额
         */
        public synchronized void drawMoney(double money) {
            // 查看是谁来取钱
            String name = Thread.currentThread().getName();
    
            // 1.判断金额是否足够
            if (this.money >= money) {
                // 2.取钱
                System.out.println(name + "取钱成功,取出:" + money);
    
                // 3.更新余额
                // 此处将更新余额操作放在第三步而不是第二步 是为了模拟出现线程安全的场景
                this.money = this.money - money;
    
                System.out.println(name + "取钱后剩余:" + this.money);
            } else {
                System.out.println(name + "来取钱,余额不足!");
            }
        }
    
    • 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. 是同步代码块好还是同步方法好一点?
      • 同步代码块锁的范围更小,同步方法锁的范围更大。
      • 同步方法可读性好,方便。

    总结:

    1. 同步方法是如何保证线程安全的?
      • 对出现问题的核心方法使用synchronized修饰。
      • 每次只能一个线程占锁进入访问。
    2. 同步方法的同步锁对象的原理?
      • 对于实例方法默认使用this作为锁对象。
      • 对于静态方法默认使用类名.class对象作为锁对象。

    d.方式三:Lock锁

    Lock锁:

    • 为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。
    • Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
    • Lock是接口不能直接实例化,这里采用它的实现类ReetrantLock来构建Lock锁对象。
    方法名称说明
    public ReentrantLock()获得Lock锁的实现类对象。
     class X {
       private final ReentrantLock lock = new ReentrantLock();
       // ...
    
       public void m() {
         lock.lock();  // block until condition holds
         try {
           // ... method body
         } finally {
           lock.unlock();
         }
       }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    Lock的API:

    方法名称说明
    void lock()获得锁。
    void unlock()释放锁。
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class Account {
        private String cardId;
        private double money;
    
        // final修饰后 锁对象是唯一和不可替换的 非常专业
        private final Lock lock = new ReentrantLock();
    
        public String getCardId() {
            return cardId;
        }
    
        public void setCardId(String cardId) {
            this.cardId = cardId;
        }
    
        public double getMoney() {
            return money;
        }
    
        public void setMoney(double money) {
            this.money = money;
        }
    
        /**
         * 静态方法使用 类名.class 作为锁对象
         */
        public static void run() {
            synchronized (Account.class) {
    
            }
        }
    
        public Account() {
        }
    
        public Account(String cardId, double money) {
            this.cardId = cardId;
            this.money = money;
        }
    
        /**
         * 取钱
         * @param money 取出金额
         */
        public void drawMoney(double money) {
            // 查看是谁来取钱
            String name = Thread.currentThread().getName();
    
            // 1.判断金额是否足够
            // 上锁
            lock.lock();
            try {
                if (this.money >= money) {
                    // 2.取钱
                    System.out.println(name + "取钱成功,取出:" + money);
    
                    // 3.更新余额
                    // 此处将更新余额操作放在第三步而不是第二步 是为了模拟出现线程安全的场景
                    this.money = this.money - money;
    
                    System.out.println(name + "取钱后剩余:" + this.money);
                } else {
                    System.out.println(name + "来取钱,余额不足!");
                }
            } finally {
                // 解锁
                lock.unlock();
            }
        }
    }
    
    
    • 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
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74

    5.线程通信

    什么是线程通向、如何实现?

    • 所谓线程通信就是线程间相互发送数据,线程通信通常通过共享一个数据的方式实现。
    • 线程间会根据共享数据的情况决定自己该怎么做,以及怎么通知其他线程怎么做。
    • 线程通信的前提:线程通信通常是在多个线程操作同一个共享资源的时候需要进行通信,且要保证线程安全。

    线程通信常见模型:

    • 生产者与消费者模型:生产者线程负责生成数据,消费者线程负责消费数据。
    • 要求:生产者线程生产完数据后,唤醒消费者,然后等待自己;消费者消费完该数据后,唤醒生产者,然后等待自己。

    Object类的等待和唤醒方法:

    方法名称说明
    void wait()让当前线程等待并释放所占所,直到另一个线程调用notify()方法或notifyAll()方法。
    void notify()唤醒正在等待的单个线程。
    void notifyAll()唤醒正在等待的所有线程。

    注意:

    • 上述方法应该使用当前同步锁对象进行调用。

    线程通信案例模拟:

    • 假如有这样一个场景,甲乙丙负责存钱,小明和小红负责取钱,必须一存、一取。

      存钱
      存钱
      存钱
      取钱
      取钱
      账户
      小明
      小红

    Account.java

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class Account {
        private String cardId;
        private double money;
    
        public String getCardId() {
            return cardId;
        }
    
        public void setCardId(String cardId) {
            this.cardId = cardId;
        }
    
        public double getMoney() {
            return money;
        }
    
        public void setMoney(double money) {
            this.money = money;
        }
    
        public Account() {
        }
    
        public Account(String cardId, double money) {
            this.cardId = cardId;
            this.money = money;
        }
    
        /**
         * 取钱
         *
         * @param money 取钱金额
         */
        public synchronized void drawMoney(int money) {
            String name = Thread.currentThread().getName();
    
            try {
                if (this.money >= money) {
                    // 钱足够 可取
                    this.money -= money;
    
                    System.out.println(name + "取钱" + money + "成功,余额为:" + this.money);
    
                    // 因为一存一取 没有钱了
                    this.notifyAll();
                    this.wait();
                } else {
                    // 钱不够 不可取 唤醒存钱线程 等待自己
                    // 唤醒所有线程
                    this.notifyAll();
    
                    // 锁对象 让当前线程进入等待
                    this.wait();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 存钱
         * @param money 存入金额
         */
        public synchronized void deposit(int money) {
            String name = Thread.currentThread().getName();
    
            try {
                if (this.money == 0) {
                    // 如果账户余额为0 存入
                    this.money += money;
    
                    System.out.println(name + "存钱" + money + "成功,余额为:" + this.money);
    
                    this.notifyAll();
                    this.wait();
                } else {
                    this.notifyAll();
                    this.wait();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    
    • 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
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88

    DrawThread.java

    /**
     * 取钱线程类
     */
    public class DrawThread extends Thread {
    
        // 账户对象
        private final Account account;
    
        public DrawThread(Account account, String threadName) {
            // 线程名称
            super(threadName);
            this.account = account;
        }
    
        @Override
        public void run() {
            // 取钱
            while (true) {
                account.drawMoney(10000);
                
                try {
                    // 休眠3秒
                    Thread.sleep(3000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    
    • 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

    DepositThread.java

    public class DepositThread extends Thread {
        private Account account;
        
        public DepositThread(Account account, String name) {
            super(name);
            this.account = account;
        }
    
        @Override
        public void run() {
    
            // 存钱
            while (true) {
                account.deposit(10000);
    
                try {
                    // 休眠3秒
                    Thread.sleep(3000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    
    • 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

    Test.java

    /**
     * 了解线程通信的流程
     */
    public class Test {
        public static void main(String[] args) {
            // 甲乙丙存钱 小明小红取钱
            // 模拟线程通信思想 一存10000 一取10000
            // 1.创建账户对象 代表5个人共同操作的账户
            Account account = new Account("BOC-7979", 0);
    
            // 2.创建2个取钱线程 代表小明和小红
            new DrawThread(account, "小明").start();
            new DrawThread(account, "小红").start();
    
            // 3.创建3个存钱线程 代表甲乙丙
            new DepositThread(account, "甲").start();
            new DepositThread(account, "乙").start();
            new DepositThread(account, "丙").start();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    执行结果:

    甲存钱10000成功,余额为:10000.0
    小红取钱10000成功,余额为:0.0
    丙存钱10000成功,余额为:10000.0
    小红取钱10000成功,余额为:0.0
    丙存钱10000成功,余额为:10000.0
    小明取钱10000成功,余额为:0.0
    甲存钱10000成功,余额为:10000.0
    小红取钱10000成功,余额为:0.0
    丙存钱10000成功,余额为:10000.0
    小明取钱10000成功,余额为:0.0
    甲存钱10000成功,余额为:10000.0
    小红取钱10000成功,余额为:0.0
    丙存钱10000成功,余额为:10000.0
    小明取钱10000成功,余额为:0.0
    甲存钱10000成功,余额为:10000.0
    小明取钱10000成功,余额为:0.0
    甲存钱10000成功,余额为:10000.0
    小红取钱10000成功,余额为:0.0
    乙存钱10000成功,余额为:10000.0
    小明取钱10000成功,余额为:0.0
    甲存钱10000成功,余额为:10000.0
    小明取钱10000成功,余额为:0.0
    丙存钱10000成功,余额为:10000.0
    小红取钱10000成功,余额为:0.0
    甲存钱10000成功,余额为:10000.0
    小明取钱10000成功,余额为:0.0
    甲存钱10000成功,余额为:10000.0
    小明取钱10000成功,余额为:0.0
    甲存钱10000成功,余额为:10000.0
    小明取钱10000成功,余额为:0.0
    甲存钱10000成功,余额为:10000.0
    小明取钱10000成功,余额为:0.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
    • 32
    • 33

    6.线程池

    a.线程池概念

    什么是线程池?

    • 线程池就是一个可以复用线程的技术。

    不是使用线程池的问题:

    • 如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能。

    b.线程池实现的API、参数说明

    谁代表线程池?

    • JDK 5.0起提供了代表线程池的接口:ExecutorService

    如何得到线程池对象:

    • 方式一:使用ExecutorService的实体类ThreadPoolExecutor自创建一个线程池对象。

      ExecutorService-接口
      ThreadPoolExecutor-实现类
    • 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。

    ThreadPoolExecutor构造器的参数说明:

        public ThreadPoolExecutor(
            int corePoolSize, 
            int maximumPoolSize, 
            long keepAliveTime, 
            TimeUnit unit, 
            BlockingQueue<Runnable> workQueue, 
            ThreadFactory threadFactory, 
            RejectedExecutionHandler handler
        )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • corePoolSize:指定线程池的线程数量(核心线程) —— 不能小于0。
    • maximumPoolSize:指定线程池可支持的最大线程数 —— 最大数量 >= 核心线程数量。
    • keepAliveTime:指定临时线程的最大存活时间 —— 不能小于0。
    • unit:指定存活时间的单位(秒、分、时、天)——时间单位。
    • workQueue:指定任务队列(线程都满了时,新任务放置在哪个队列中) ——不能为null。
    • threadFactory:指定用哪个线程工厂创建线程 —— 不能为null。
    • handler:指定线程忙,任务满的时候,新任务来了怎么办? —— 不能为null。

    线程池常见面试题:

    1. 临时线程什么时候创建?
      • 新任务提交是发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
    2. 什么时候会开始拒绝任务?
      • 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会才开始任务拒绝。

    c.线程池处理Runnable任务

    ThreadPoolExecutor创建线程池对象实例:

            ExecutorService executorService = new ThreadPoolExecutor(
                    3,
                    5,
                    8,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(6),
                    Executors.defaultThreadFactory(),
                    new ThreadPoolExecutor.AbortPolicy()
            );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    ExecutorService的常用方法:

    方法名称说明
    void execute(Runnable command)执行任务/命令,没有返回值,一般用来执行Runnable任务。
    Future submit(Callable task)执行任务,返回未来任务对象获取线程结果,一般拿来执行Callable任务。
    void shutdown()等任务执行完毕后关闭线程池。
    List shutdownNow()立刻关闭,停止正在执行的任务,并返回队列中未执行的任务。

    新任务拒绝策略:

    策略详解
    ThreadPoolExecutor.AbortPolicy丢弃任务并抛出RejectedExecutionException异常。(默认策略)
    ThreadPoolExecutor.DiscardPolicy丢弃任务,但是不抛出异常,这是不推荐的做法。
    ThreadPoolExecutor.DiscardOldestPolicy抛弃队列中等待最久的任务,然后把当前任务加入队列中。
    ThreadPoolExecutor.CallerRunsPolicy由主线程负责调用任务的run()方法从而绕过线程池直接执行。

    Test.java

    import java.util.concurrent.*;
    
    public class Test {
        public static void main(String[] args) {
    
            /* 1.创建线程池对象 3个核心线程 5个最大线程 临时线程最大时间为8秒 任务队列长度为3 */
            ExecutorService executorService = new ThreadPoolExecutor(
                    3,
                    5,
                    8,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(3),
                    Executors.defaultThreadFactory(),
                    new ThreadPoolExecutor.AbortPolicy()
            );
    
            /* 2.给任务线程池处理 */
            Runnable runnable = new MyRunnable();
    
            // 三个核心线程 处理多个任务
            executorService.execute(runnable);
            executorService.execute(runnable);
            executorService.execute(runnable);
            executorService.execute(runnable);
            executorService.execute(runnable);
            executorService.execute(runnable);
            // 线程数量 > 核心线程数 + 任务队列长度  开始创建临时线程
            executorService.execute(runnable);
            executorService.execute(runnable);
            try {
                // 核心线程和临时线程都在忙 任务队列已满 开始拒绝任务
                executorService.execute(runnable);
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            /* 4.关闭线程池 开发中一般不会使用 */
            // 立即关闭 即使任务没有完成 会丢失任务
            // executorService.shutdownNow();
            // 等待全部任务完成之后关闭
            executorService.shutdown();
        }
    }
    
    /**
     * 自定义线程类
     */
    class MyRunnable implements Runnable {
    
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "输出了:HelloWorld" + i);
    
            }
    
            try {
                System.out.println(Thread.currentThread().getName() + "本任务与线程绑定,线程进入休眠~~~");
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    • 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

    执行结果:

    java.util.concurrent.RejectedExecutionException: Task com.java.threadpooldemo.MyRunnable@2a84aee7 rejected from java.util.concurrent.ThreadPoolExecutor@a09ee92[Running, pool size = 5, active threads = 5, queued tasks = 3, completed tasks = 0]
    	at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2065)
    	at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:833)
    	at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1365)
    	at com.java.threadpooldemo.Test.main(Test.java:34)
    pool-1-thread-4输出了:HelloWorld0
    pool-1-thread-4输出了:HelloWorld1
    pool-1-thread-4输出了:HelloWorld2
    pool-1-thread-4输出了:HelloWorld3
    pool-1-thread-4输出了:HelloWorld4
    pool-1-thread-1输出了:HelloWorld0
    pool-1-thread-5输出了:HelloWorld0
    pool-1-thread-5输出了:HelloWorld1
    pool-1-thread-5输出了:HelloWorld2
    pool-1-thread-5输出了:HelloWorld3
    pool-1-thread-5输出了:HelloWorld4
    pool-1-thread-5本任务与线程绑定,线程进入休眠~~~
    pool-1-thread-4本任务与线程绑定,线程进入休眠~~~
    pool-1-thread-2输出了:HelloWorld0
    pool-1-thread-3输出了:HelloWorld0
    pool-1-thread-3输出了:HelloWorld1
    pool-1-thread-3输出了:HelloWorld2
    pool-1-thread-3输出了:HelloWorld3
    pool-1-thread-3输出了:HelloWorld4
    pool-1-thread-3本任务与线程绑定,线程进入休眠~~~
    java.lang.InterruptedException: sleep interrupted
    	at java.base/java.lang.Thread.sleep(Native Method)
    	at com.java.threadpooldemo.MyRunnable.run(Test.java:59)
    	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
    	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
    	at java.base/java.lang.Thread.run(Thread.java:833)
    java.lang.InterruptedException: sleep interrupted
    	at java.base/java.lang.Thread.sleep(Native Method)
    	at com.java.threadpooldemo.MyRunnable.run(Test.java:59)
    	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
    	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
    	at java.base/java.lang.Thread.run(Thread.java:833)
    java.lang.InterruptedException: sleep interrupted
    	at java.base/java.lang.Thread.sleep(Native Method)
    	at com.java.threadpooldemo.MyRunnable.run(Test.java:59)
    	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
    	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
    	at java.base/java.lang.Thread.run(Thread.java:833)
    java.lang.InterruptedException: sleep interrupted
    	at java.base/java.lang.Thread.sleep(Native Method)
    	at com.java.threadpooldemo.MyRunnable.run(Test.java:59)
    	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
    	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
    	at java.base/java.lang.Thread.run(Thread.java:833)
    java.lang.InterruptedException: sleep interrupted
    	at java.base/java.lang.Thread.sleep(Native Method)
    	at com.java.threadpooldemo.MyRunnable.run(Test.java:59)
    	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
    	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
    	at java.base/java.lang.Thread.run(Thread.java:833)
    pool-1-thread-2输出了:HelloWorld1
    pool-1-thread-2输出了:HelloWorld2
    pool-1-thread-2输出了:HelloWorld3
    pool-1-thread-2输出了:HelloWorld4
    pool-1-thread-2本任务与线程绑定,线程进入休眠~~~
    pool-1-thread-1输出了:HelloWorld1
    pool-1-thread-1输出了:HelloWorld2
    pool-1-thread-1输出了:HelloWorld3
    pool-1-thread-1输出了:HelloWorld4
    pool-1-thread-1本任务与线程绑定,线程进入休眠~~~
    
    Process finished with exit code 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
    • 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
    • 66
    • 67
    • 68

    d.线程池处理Callable任务

    ExecutorSerivce的常用方法:

    方法名称说明
    void execute(Runnable command)执行任务/命令,没有返回值,一般用来执行Runnable任务。
    Future submit(Callable task)执行Callable任务,返回未来任务对象获取线程结果。
    void shutdown()等任务执行完毕后关闭线程池。
    List shutdownNow()立刻关闭,停止正在执行的任务,并返回队列中未执行的任务。

    MyCallable.java

    import java.util.concurrent.Callable;
    
    /**
     * 1.定义一个任务类 实现Callable借口 应该申明线程任务执行完毕后的结果的数据类型
     */
    public class MyCallable implements Callable<String> {
    
        private int number;
    
        public MyCallable(int number) {
            this.number = number;
        }
    
    
        /**
         * 2.重写call方法
         *
         * @return
         * @throws Exception
         */
        @Override
        public String call() throws Exception {
            int sum = 0;
    
            for (int i = 0; i <= number; i++) {
                sum += i;
            }
    
            return Thread.currentThread().getName() + "计算1-" + number + "的和是:" + sum;
        }
    }
    
    
    • 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

    Test.java

    import java.util.concurrent.*;
    
    public class Test {
        public static void main(String[] args) throws Exception {
            /* 1.创建线程池对象 3个核心线程 5个最大线程 临时线程最大时间为8秒 任务队列长度为3 */
            ExecutorService executorService = new ThreadPoolExecutor(
                    3,
                    5,
                    8,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(3),
                    Executors.defaultThreadFactory(),
                    new ThreadPoolExecutor.AbortPolicy()
            );
    
            /* 2.给任务线程池处理 */
            Future<String> future1 = executorService.submit(new MyCallable(100));
            Future<String> future2 = executorService.submit(new MyCallable(200));
            Future<String> future3 = executorService.submit(new MyCallable(300));
            Future<String> future4 = executorService.submit(new MyCallable(400));
            Future<String> future5 = executorService.submit(new MyCallable(500));
    
            // 3.取出结果
            System.out.println(future1.get());
            System.out.println(future2.get());
            System.out.println(future3.get());
            System.out.println(future4.get());
            System.out.println(future5.get());
        }
    }
    
    
    • 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

    执行结果:

    pool-1-thread-1计算1-100的和是:5050
    pool-1-thread-2计算1-200的和是:20100
    pool-1-thread-3计算1-300的和是:45150
    pool-1-thread-3计算1-400的和是:80200
    pool-1-thread-3计算1-500的和是:125250
    
    • 1
    • 2
    • 3
    • 4
    • 5

    e.Executors工具类实现线程池

    Executors得到线程池对象的常用方法:

    • Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。
    方法名称说明
    public static ExecutorService newCachedThreadPool()线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间则会被回收掉。
    public static ExecutorService newFixedThreadPool(int nThreads)创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
    public static ExecutorService newSingleThreadExecutor()创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。

    注意:Executors的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的。

    Test.java

    import java.util.concurrent.*;
    
    /**
     * 目标:使用Executors的工具方法直接得到一个线程池对象
     */
    public class Test {
        public static void main(String[] args) throws Exception {
            /* 1.创建固定线程数据的线程池 */
            ExecutorService executorService = Executors.newFixedThreadPool(3);
    
            executorService.execute(new MyRunnable());
            executorService.execute(new MyRunnable());
            executorService.execute(new MyRunnable());
            // 已经没有多余线程来处理
            executorService.execute(new MyRunnable());
        }
    }
    
    /**
     * 自定义线程类
     */
    class MyRunnable implements Runnable {
    
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "输出了:HelloWorld" + i);
    
            }
    
            try {
                System.out.println(Thread.currentThread().getName() + "本任务与线程绑定,线程进入休眠~~~");
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    
    • 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

    Executors使用可能存在的陷阱:

    • 大型并发系统中使用Executors如果不注意可能会出现系统风险。
    方法名称存在问题
    public static ExecutorService newFixedThreadPool(int nThreads)允许请求的任务队列长度是Integer.MAX_VALUE,可能出现OOM错误(java.lang.OutOfMemoryError)。
    public static ExecutorService newSingleThreadExector()
    public static ExecutorService newCachedThreadPool()创建的线程数量最大上限是Integer.MAX_VALUE,线程数量可能会随着任务1:1增长,也可能出现OOM错误(java.lang.OutOfMemoryError)。
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

    阿里巴巴Java开发手册中这样写道:

    4.【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样
    的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
    说明:Executors 返回的线程池对象的弊端如下:

    1. FixedThreadpool 和 SingleThreadPool:
      允许的谐求队列长度为 Integez.MAX_ VALUE,可能会堆积大量的请求,从而导致 OOM。
    2. CachedThreadPool fl ScheduledThreadPool:
      允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

    7.补充知识:定时器

    定时器:

    • 定时器是一种控制任务延时调用,或者周期调用的技术。
    • 作用:闹钟、定时右键发送。

    定时器的实现方式:

    • 方式一:Timer
    • 方式二:ScheduledExecutorService

    Timer定时器:

    构造器说明
    public Timer()创建Timer定时器对象。
    方法说明
    public void schedule(TimerTask task, long delay, long period)开启一个定时器,按照计划处理TimerTask任务。

    Test.java

    import java.util.Timer;
    import java.util.TimerTask;
    
    /**
     * 目标:Timer定时器的使用和了解
     */
    public class Test {
        public static void main(String[] args) {
            // 1.创建Timer定时器
            // 定时器本身就是一个单线程
            Timer timer = new Timer();
    
            // 2.调用方法 处理定时任务
            // 3秒后 开始执行 之后每过2秒启动一次
            timer.schedule(new TimerTask() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "执行一次!");
                }
            }, 3000, 2000);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    Timer定时器的特点和存在的问题:

    1. Timer是单线程,处理多个任务按照顺序hi子那个,存在延时与设置定时器的时间有出入。
    2. 可能是因为其中的某个任务的异常使Timer线程死掉,从而影响后续任务执行。

    Test.java

    import java.util.Date;
    import java.util.Timer;
    import java.util.TimerTask;
    
    /**
     * 目标:Timer定时器的使用和了解
     */
    public class Test {
        public static void main(String[] args) {
            // 1.创建Timer定时器
            // 定时器本身就是一个单线程
            Timer timer = new Timer();
    
            // 2.调用方法 处理定时任务
            // 3秒后 开始执行 之后每过2秒启动一次
            timer.schedule(new TimerTask() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "执行一次A!" + new Date());
    
                    // 休眠5秒 影响其他任务执行
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, 0, 2000);
    
            timer.schedule(new TimerTask() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "执行一次B!" + new Date());
                    
                    // 异常使得Timer线程死掉 影响后续任务执行
                    // System.out.println(10 / 0);
                }
            }, 0, 2000);
    
            timer.schedule(new TimerTask() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "执行一次C!" + new Date());
                }
            }, 0, 2000);
        }
    }
    
    
    • 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

    ScheduledExecutorService定时器:

    • ScheduledExecutorService是jdk5.1中引入的并发包,目的是为了弥补Timer的缺陷,ScheduledExecutorService内部为线程池。
    Executors的方法说明
    public static ScheduleExecutorService newScheduledThreadPool(int corePoolSize)得到线程池对象。
    ScheduleExecutorService方法说明
    public ScheduleFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)周期调用方法。

    ScheduledExecutorService的特点:

    1. 基于线程池,某个任务的执行情况不会影响其他定时任务的执行。

    Test.java

    import java.util.Date;
    import java.util.TimerTask;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    
    public class Test {
        public static void main(String[] args) {
            // 1.创建ScheduledExecutorService线程池 做定时器
            ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
    
            // 2.开启定时任务
            scheduledExecutorService.scheduleAtFixedRate(new TimerTask() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "执行输出:A" + new Date());
    
                    // 休眠 不影响其他任务执行
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }, 0, 2, TimeUnit.SECONDS);
    
            scheduledExecutorService.scheduleAtFixedRate(new TimerTask() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "执行输出:B " + new Date());
    
                    // 出现异常 不影响其他任务执行
                    System.out.println(10 / 0);
                }
            }, 0, 2, TimeUnit.SECONDS);
    
            scheduledExecutorService.scheduleAtFixedRate(new TimerTask() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "执行输出:C " + new Date());
                }
            }, 0, 2, TimeUnit.SECONDS);
        }
    }
    
    
    • 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

    8.补充知识:并发并行

    并发与并行:

    • 正在运行的程序(软件)就是一个独立的进程,线程是属于进程的,多个线程其实是并发与并行同时进行的。

    并发的理解:

    • CPU同时处理线程的数量有限。
    • CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。

    并行的理解:

    • 在同一时刻上,同时有多个线程在被CPU处理并执行。

    总结:

    1. 简单说说并发和并行的含义?
      • 并发:CPU分时轮询的执行线程。
      • 并行:多个CPU同一个时刻在执行线程。

    9.补充知识:线程的生命周期

    线程的状态:

    • 线程的状态:也就是线程从生到死的过程,以及中间经历的各种状态及状态转换。
    • 理解线程的状态有利于提升并发编程的理解能力。

    Java线程的状态:

    • Java总共定义了6中状态。
    • 6中状态都定义在Thread类的内部枚举类中。
        public static enum State {
            // 1.创建
            NEW,
            // 2.可运行
            RUNNABLE,
            // 3.锁阻塞
            BLOCKED,
            // 4.无限等待
            WAITING,
            // 5.计时等待
            TIMED_WAITING,
            // 6.被终止
            TERMINATED;
    
            private State() {
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    线程的6种状态互相转换:

    调用start
    执行完毕或出现异常
    未获得锁
    获取锁
    获得锁调用wait
    被其他线程notify但未获得锁
    被其他线程notify并获得锁
    调用sleep或wait带毫秒参数
    1.sleep时间到, 2.wait时间到并获得锁, 3.wait时间没到被其他线程notify并获得锁
    1.wait时间到未获得锁, 2.wait时间没到被其他线程notify未获得锁对象
    NEW 新建
    RUNNABLE 可运行
    TERMINATED 被终止
    BLOCKED 锁阻塞
    WAITING 无限等待
    TIMED_WAITING 计时等待

    线程的6种状态总结:

    线程状态描述
    NEW 新建线程刚被创建,但是并未启动。创建线程对象。
    RUNNABLE 可运行线程已经被调用了start()等待CPU调度。start()方法。
    BLOCKED 锁阻塞线程在执行的时候未竞争到锁对象,则该线程进入BLOCKED状态。无法获得锁对象。
    WAITING 无限等待一个线程进入WAITING状态,另一个线程调用notify()或者notifyAll方法才能够唤醒。wait()方法
    TIMED_WAITING 计时等待同WAITING状态,有几个方法有超时参数,调用他们将进入TIMED_WAITING状态。带有超时参数的常用方法有Thread.sleep、Object.wait。sleep()方法
    TERMINATED 被终止因为run()方法正常退出而死亡,或者因为没有捕获的异常终止了run()方法而死亡。代码运行完毕。
  • 相关阅读:
    经纬恒润预期功能安全(SOTIF)解决方案为自动驾驶安全保驾护航
    lightdb支持cast(expr as unsigned)-mysql兼容
    上传项目到github上
    LLM推理入门指南①:文本生成的初始化与解码阶段
    【DS】二叉搜索树的介绍和实现
    CF1574B Combinatorics Homework
    基础算法练习200题13、判断质数
    关于如何计算 递归 方法 的时间复杂度 笔记总结
    Scrum指南笔记
    软件测试周刊(第78期):你对未来越有信心,你对现在越有耐心。
  • 原文地址:https://blog.csdn.net/weixin_42856871/article/details/127706298