• 【Java从入门到大牛】多线程


    🔥 本文由 程序喵正在路上 原创,CSDN首发!
    💖 系列专栏:Java从入门到大牛
    🌠 首发时间:2023年11月18日
    🦋 欢迎关注🖱点赞👍收藏🌟留言🐾

    多线程的创建

    方式一:继承Thread类

    什么是线程?

    线程(Thread)是一个程序内的一条执行流程,比如常见的 main 方法就是一个线程

    public static void main(String[] args) {
    	// 代码...
    	for(int i = 0; i <10; i++) {
    		System.out.println(i);
    	}
    	//...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    程序中如果只有一条执行流程,那么这个程序就是单线程的程序

    什么是多线程?

    多线程是指从软硬件上实现的多条执行流程的技术(多条线程由 CPU 负责调度执行)

    多线程的用处

    在生活中,有很多多线程应用的例子,比如,12306网站同时有多位用户在抢票,那么网站就需要提供多条线程来为多位用户服务;在使用百度网盘时,用户可能同时需要上传和下载资源,这也需要用到多线程技术;再比如消息通信、淘宝、京东系统都离不开多线程技术

    如果在程序中创建出多条线程呢?

    Java 是通过 java.lang.Thread 类的对象来代表线程的

    多线程的创建方式一:继承Thread类

    1. 定义一个子类 MyThread 继承线程类 java.lang.Thread,并重写 run() 方法
    2. 创建 MyThread 类的对象
    3. 调用线程对象的 start() 方法启动线程,这个方法会自动执行 run() 方法

    代码实现

    MyThread类

    /**
     * 1. 让子类继承Thread线程类
     */
    public class MyThread extends Thread {
        @Override   // 2. 必须重写Thread类的run方法
        public void run() {
            // 这里用来描述线程要执行的任务
            for (int i = 1; i <= 5; i++) {
                System.out.println("子线程MyThread输出:" + i);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    测试类

    /**
     * 目标:掌握线程的创建方式一:继承Thread类
     */
    public class ThreadTest1 {
        // main方法是由一条默认的主线程负责执行的
        public static void main(String[] args) {
            // 3. 创建MyThread线程类的对象代表一个线程
            Thread t = new MyThread();  // 多态写法
    
            // 4. 启动线程,必须是调用start方法
            t.start();  // 启动后,现在就存在两个线程:main线程和t线程
    
            for (int i = 1; i <= 5; i++) {
                System.out.println("主线程main输出:" + i);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    因为有两个线程,线程执行的顺序也不确定,所有每次执行结果都可能不同

    在这里插入图片描述

    方法一优缺点:

    • 优点:编码简单
    • 缺点:线程类已经继承 Thread 类,所以无法再继承其他类,不利于功能的扩展

    多线程的注意事项

    1、启动线程必须是调用 start 方法,不能调用 run 方法

    • 直接调用 run 方法会当成普通方法执行,此时相当于还是单线程执行
    • 只有调用 start 方法才是启动一个新的线程执行

    2、不要把主线程任务放在启动子线程之前

    • 这样主线程一直是先跑完的,相当于是一个单线程的效果了

    方式二:实现Runnable接口

    多线程的创建方式二:实现Runnable接口

    1. 定义一个线程任务类 MyRunnable 实现 Runnable 接口,并重写 run() 方法

    2. 创建 MyRunnable 任务对象

    3. 把 MyRunnable 任务对象交给 Thread 处理
      在这里插入图片描述

    4. 调用线程对象的 start() 方法启动线程

    代码实现

    MyRunnable类

    /**
     * 1.定义一个任务类,实现Runnable接口
     */
    public class MyRunnable implements Runnable {
        // 2.重写Runnable的run方法
        @Override
        public void run() {
            // 描述线程要执行的任务
            for (int i = 1; i <= 5; i++) {
                System.out.println("子线程输出:" + i);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    测试类

    /**
     * 目标:掌握多线程的创建方式二:实现Runnable接口
     */
    public class ThreadTest2 {
        public static void main(String[] args) {
            // 3.创建任务对象
            Runnable target = new MyRunnable();
            // 4.把任务对象交给一个线程对象处理
            new Thread(target).start();
    
            for (int i = 1; i <= 5; i++) {
                System.out.println("主线程main输出:" + i);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    方式二的优缺点

    • 优点:任务类只是实现接口,可以继承其他类、实现其他接口,扩展性强
    • 缺点:需要多一个 Runnable 对象

    线程创建方式二的匿名内部类写法

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

    匿名内部类写法实现

    /**
     * 目标:掌握多线程创建方式二的匿名内部类写法
     */
    public class ThreadTest2_2 {
        public static void main(String[] args) {
            // 1、直接创建Runnable接口的匿名内部类形式(任务对象)
            Runnable target = new Runnable() {
                @Override
                public void run() {
                    for (int i = 1; i <= 5; i++) {
                        System.out.println("子线程1输出:" + i);
                    }
                }
            };
            new Thread(target).start();
    
            // 简化形式1:
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 1; i <= 5; i++) {
                        System.out.println("子线程2输出:" + i);
                    }
                }
            }).start();
    
            // 简化形式2:
            new Thread(() -> {
                    for (int i = 1; i <= 5; i++) {
                        System.out.println("子线程3输出:" + i);
                    }
            }).start();
    
            for (int i = 1; i <= 5; i++) {
                System.out.println("主线程main输出:" + 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

    方式三:实现Callable接口

    前两种线程创建方式都存在一个问题:假如线程执行完毕后有一些数据需要返回,他们重写的 run 方法均不能返回结果

    怎么解决这个问题?

    • JDK5.0 提供了 Callable 接口和 FutureTask 来实现
    • 这种方式最大的优点就是可以返回线程执行完毕后的结果

    多线程的第三种创建方式:利用Callable接口和FutureTask类来实现

    1、创建任务对象

    • 定义一个类实现 Callable 接口,并重写 call 方法,里面封装要做的事情和要返回的数据
    • 把 Callable 类型的对象封装成 FutureTask(线程任务对象)

    2、把线程任务对象交给 Thread 对象

    3、调用 Thread 对象的 start 方法启动线程

    4、线程执行完毕后,通过 FutureTask 对象的 get 方法去获取线程任务执行的结果

    FutureTask的API

    在这里插入图片描述
    代码实现

    MyCallable类

    import java.util.concurrent.Callable;
    
    /**
     * 1、让这个类实现Callable接口
     */
    public class MyCallable implements Callable<String> {
        private int n;
        public MyCallable(int n) {
            this.n = n;
        }
    
        // 2、重写call方法
        @Override
        public String call() throws Exception {
            // 描述线程的任务,返回线程执行返回后的结果
            // 需求:求1-n的和并返回
            int sum = 0;
            for (int i = 1; i <= n; i++) {
                sum += i;
            }
            return "线程求出了1-" + n + "的和是:" + sum;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    测试类

    import java.util.concurrent.Callable;
    import java.util.concurrent.FutureTask;
    
    /**
     * 目标:掌握线程的创建方式三:实现Callable接口
     */
    public class ThreadTest3 {
        public static void main(String[] args) throws Exception {
            // 3、创建一个Callable的对象
            Callable<String> call = new MyCallable(100);
            // 4、把Callable的对象封装成一个FutureTask对象(任务对象)
            // 未来任务对象的作用?
            // 1、是一个任务对象,实现了Runnable对象
            // 2、可以在线程执行完毕之后,用未来任务对象调用get方法获取线程执行完毕后的结果
            FutureTask<String> f1  = new FutureTask<>(call);
            // 5、把任务对象交给一个Thread对象
            new Thread(f1).start();
    
            Callable<String> call2 = new MyCallable(200);
            FutureTask<String> f2  = new FutureTask<>(call2);
            new Thread(f2).start();
    
            // 6、获取线程执行完毕后返回的结果
            // 注意:如果执行到这儿,假如上面的线程还没有执行完毕
            // 这里的代码会暂停,等待上面线程执行完毕后才会获取结果
            String rs = f1.get();
            System.out.println(rs);
    
            String rs2 = f2.get();
            System.out.println(rs2);
        }
    }
    
    • 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

    在这里插入图片描述

    线程创建方式三的优缺点

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

    Thread的常见方法

    Thread 提供了很多与线程操作相关的方法
    在这里插入图片描述

    应用演示

    MyThread类

    public class MyThread extends Thread {
        public MyThread(String name) {
            super(name);    // 为当前线程设置名字
        }
        @Override
        public void run() {
            // 获取当前线程对象
            Thread t = Thread.currentThread();
            for (int i = 1; i <= 3; i++) {
                System.out.println(t.getName() + "输出:" + i);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    测试类1

    /**
     * 目标:掌握Thread的常用方法
     */
    public class ThreadTest1 {
        public static void main(String[] args) {
            Thread t1 = new MyThread("1号线程");
            t1.start();
            System.out.println(t1.getName());   // Thread-0
    
            Thread t2 = new MyThread("2号线程");
            t2.start();
            System.out.println(t2.getName());   // Thread-1
    
            //获取主线程对象的名字
            Thread m = Thread.currentThread();
            m.setName("main主线程");
            System.out.println(m.getName());
    
            for (int i = 1; i <= 5; i++) {
                System.out.println(m.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

    在这里插入图片描述

    测试类2

    /**
     * 目标:掌握sleep方法,join方法的作用
     */
    public class ThreadTest2 {
        public static void main(String[] args) throws Exception {
            for (int i = 1; i <= 5; i++) {
                System.out.println(i);
                // 休眠5s
                if(i == 3){
                    // 让当前执行的线程暂停5秒,再继续执行
                     Thread.sleep(5000);
                }
            }
    
            // join方法作用:让当前调用这个方法的线程先执行完再执行其他线程
            Thread t1 = new MyThread("1号线程");
            t1.start();
            t1.join();
    
            Thread t2 = new MyThread("2号线程");
            t2.start();
            t2.join();
    
            Thread t3 = new MyThread("3号线程");
            t3.start();
            t3.join();
        }
    }
    
    • 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

    在这里插入图片描述

    Thread其他方法的说明

    Thread 类还提供了诸如:yield、interrupt、守护线程、线程优先级等线程的控制方法,在开发中很少使用,在这里就不一一列出了

    线程安全

    什么是线程安全问题

    什么是线程安全问题?

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

    取钱的线程安全问题

    场景:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,如果小明和小红同时来取钱,并且2人各自都在取钱10万元,可能会出现什么问题呢?

    有可能会出现2人都成功取钱10万元的结果

    用程序模拟线程安全问题

    需求

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

    分析

    1. 需要提供一个账户类,接着创建一个账户对象代表2个人的共享账户
    2. 需要定义一个线程类(用于创建两个线程,分别代表小明和小红)
    3. 创建两个线程,传入同一个账户对象给两个线程处理
    4. 启动两个线程,同时去同一个账户对象中取钱10万元

    代码实现

    账户类

    public class Account {
        private String cardId; // 卡号
        private double money; // 余额。
    
        public Account() {
        }
    
        public Account(String cardId, double money) {
            this.cardId = cardId;
            this.money = money;
        }
    
        // 取钱
        public void drawMoney(double money) {
            // 先搞清楚是谁来取钱?
            String name = Thread.currentThread().getName();
            // 1、判断余额是否足够
            if(this.money >= money){
                System.out.println(name + "来取钱" + money + "成功!");
                this.money -= money;
                System.out.println(name + "来取钱后,余额剩余:" + this.money);
            }else {
                System.out.println(name + "来取钱:余额不足~");
            }
        }
    
        // get、set方法
        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;
        }
    }
    
    • 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

    线程类

    public class DrawThread extends Thread{
        private Account acc;
        public DrawThread(Account acc, String name){
            super(name);
            this.acc = acc;
        }
        @Override
        public void run() {
            acc.drawMoney(100000);  // 取钱
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    测试类

    /**
     * 目标:模拟线程安全问题
     */
    public class ThreadTest {
        public static void main(String[] args) {
             // 1、创建一个账户对象,代表两个人的共享账户
            Account acc = new Account("ICBC-110", 100000);
    
            // 2、创建两个线程,分别代表小明 小红,再去同一个账户对象中取钱10万
            new DrawThread(acc, "小明").start(); // 小明
            new DrawThread(acc, "小红").start(); // 小红
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    线程同步

    认识线程同步

    线程同步

    解决线程安全问题的方案

    线程同步的思想

    让多个线程实现先后依次访问共享资源,这样就解决了安全问题

    线程同步的常见方案

    加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来

    方式一:同步代码块

    同步代码块

    作用:把访问共享资源的核心代码给上锁,以此保证线程安全

    synchronized(同步锁) {
    	访问共享资源的核心代码
    }
    
    • 1
    • 2
    • 3

    原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行

    同步锁的注意事项

    对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug

    同步代码块的实现

    IDEA里加同步锁的快捷方式:鼠标选中要上锁的核心代码,按住 Ctrl+Alt+T,然后选择第9项 synchronized 即可

    依旧是前面那个取钱的例子,我们只需要将账户类稍微修改一下即可,代码如下

    public class Account {
        private String cardId; // 卡号
        private double money; // 余额
    
        // ...
    
        // 取钱
        public void drawMoney(double money) {
            // 先搞清楚是谁来取钱?
            String name = Thread.currentThread().getName();
            // 1、判断余额是否足够
            // this正好代表共享资源!
            synchronized (this) {
                if(this.money >= money){
                    System.out.println(name + "来取钱" + money + "成功!");
                    this.money -= money;
                    System.out.println(name + "来取钱后,余额剩余:" + this.money);
                }else {
                    System.out.println(name + "来取钱:余额不足~");
                }
            }
        }
    
        // 如果是静态方法,建议使用 类名.class 作为锁
        public static void test(){
            synchronized (Account.class){
    
            }
        }
    
        // ...
    }
    
    • 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

    在这里插入图片描述

    问题:锁对象随便选择一个唯一的对象好不好呢?

    不好,可能会影响其他无关线程的执行

    锁对象的使用规范

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

    方式二:同步方法

    同步方法

    作用:把访问共享资源的核心方法给上锁,以此保证线程安全

    修饰符 synchronized 返回值类型 方法名称(形参列表) {
    	操作共享资源的代码
    }
    
    • 1
    • 2
    • 3

    原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行

    同步方法底层原理

    • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码
    • 如果方法是实例方法:同步方法默认用 this 作为锁对象
    • 如果方法是静态方法:同步方法默认用 类名.class 作为锁对象

    同步方法代码实现

    账户类修改如下

    public class Account {
        private String cardId; // 卡号
        private double money; // 余额
    
        // ...
        
        // 同步方法
        public synchronized void drawMoney(double money) {
            // 先搞清楚是谁来取钱?
            String name = Thread.currentThread().getName();
            // 1、判断余额是否足够
            if(this.money >= money){
                System.out.println(name + "来取钱" + 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

    方式三:Lock锁

    Lock锁

    • Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大

    • Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象

      在这里插入图片描述

    Lock的常用方法
    在这里插入图片描述

    Lock锁代码实现

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class Account {
        private String cardId; // 卡号
        private double money; // 余额
        // 创建了一个锁对象
        private final Lock lk = new ReentrantLock();
    
        // ...
    
        // 小明 小红线程同时过来的
        public void drawMoney(double money) {
            // 先搞清楚是谁来取钱?
            String name = Thread.currentThread().getName();
            
            // 为了防止try里面的代码块出现异常而导致无法解锁,
            // 我们需要对其进行异常处理,并将解锁操作放到finally里面
            try {
                lk.lock(); // 加锁
                // 1、判断余额是否足够
                if(this.money >= money){
                    System.out.println(name + "来取钱" + money + "成功!");
                    this.money -= money;
                    System.out.println(name + "来取钱后,余额剩余:" + this.money);
                }else {
                    System.out.println(name + "来取钱:余额不足~");
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lk.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

    线程通信【了解】

    什么是线程通信?

    当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺

    线程通信的常见模型(生产者与消费者模型)

    • 生产者线程负责生产数据
    • 消费者线程负责消费生产者生产的数据
    • 注意:生产者生产完数据应该等待自己,通知消费者消费;消费者消费完也应该等待自己,再通知生产者生产

    Object类的等待和唤醒方法
    在这里插入图片描述

    注意:上述方法应该使用当前同步锁对象进行调用

    为了更好地理解生产者与消费者模型,我们来看一个例子:

    • 3个生产者线程,负责生产包子,每个线程每次只能生产1个包子放在桌子上
    • 2个消费者线程负责吃包子,每人每次只能从桌子上拿1个包子吃
    • 同一时刻,只能有一个生产者生产包子,或只能有一个消费者吃包子

    代码实现

    桌子类

    import java.util.ArrayList;
    import java.util.List;
    
    public class Desk {
        private List<String> list = new ArrayList<>();
    
        // 放1个包子的方法
        // 厨师1 厨师2 厨师3
        public synchronized void put() {
            try {
                String name = Thread.currentThread().getName();
                // 判断是否有包子
                if(list.size() == 0){
                    list.add(name + "做的肉包子");   // 没有包子,生产包子
                    System.out.println(name + "做了一个肉包子~~");
                    Thread.sleep(2000);
    
                    // 先唤醒别人, 再等待自己,顺序不能错
                    this.notifyAll();
                    this.wait();
                }else {
                    // 有包子了,不做了。
                    // 唤醒别人, 等待自己
                    this.notifyAll();
                    this.wait();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        // 吃货1 吃货2
        public synchronized void get() {
            try {
                String name = Thread.currentThread().getName();
                if(list.size() == 1){
                    // 有包子,吃了
                    System.out.println(name  + "吃了:" + list.get(0));
                    list.clear();
                    Thread.sleep(1000);
                    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

    测试类

    /**
     * 目标:了解一下线程通信
     */
    public class ThreadTest {
        public static void main(String[] args) {
            //   需求:3个生产者线程,负责生产包子,每个线程每次只能生产1个包子放在桌子上
            //      2个消费者线程负责吃包子,每人每次只能从桌子上拿1个包子吃
            Desk desk  = new Desk();
    
            // 创建3个生产者线程(3个厨师)
            new Thread(() -> {
                while (true) {
                    desk.put();
                }
            }, "厨师1").start();
    
            new Thread(() -> {
                while (true) {
                    desk.put();
                }
            }, "厨师2").start();
    
            new Thread(() -> {
                while (true) {
                    desk.put();
                }
            }, "厨师3").start();
    
            // 创建2个消费者线程(2个吃货)
            new Thread(() -> {
                while (true) {
                    desk.get();
                }
            }, "吃货1").start();
    
            new Thread(() -> {
                while (true) {
                    desk.get();
                }
            }, "吃货2").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
    • 42

    在这里插入图片描述

    线程池

    认识线程池

    什么是线程池?

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

    不使用线程池的问题

    用户每发起一个请求,系统后台就需要创建一个新线程来处理,这样下次有新任务来了又要创建新线程来处理,而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能

    线程池的工作原理
    在这里插入图片描述

    创建线程池

    Java中谁代表线程池?

    JDK5.0 起提供了代表线程池的接口:ExecutorService

    如何得到线程池对象?

    • 方式一:使用 ExecutorService 的实现类 ThreadPoolExecutor 自创建一个线程池对象
    • 方式二:使用 Executors(线程池的工具类)调用方法返回不同特点的线程池对象

    ThreadPoolExecutor的构造器

    public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
    							BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
    							RejectedExecutionHandler handler) 
    
    • 1
    • 2
    • 3
    • 参数一:corePoolSize : 指定线程池的核心线程的数量
    • 参数二:maximumPoolSize:指定线程池的最大线程数量
    • 参数三:keepAliveTime :指定临时线程的存活时间
    • 参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)
    • 参数五:workQueue:指定线程池的任务队列
    • 参数六:threadFactory:指定线程池的线程工厂
    • 参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)

    创建一个线程池

    public class ThreadPoolTest1 {
        public static void main(String[] args) {
            // 1、通过ThreadPoolExecutor创建一个线程池对象
            ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
                    TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
                    new ThreadPoolExecutor.CallerRunsPolicy());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    线程池的注意事项

    1、临时线程什么时候创建?

    • 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程

    2、什么时候会开始拒绝新任务?

    • 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务

    线程池处理Runnable任务

    ExecutorService的常用方法
    在这里插入图片描述

    新任务拒绝策略

    在这里插入图片描述

    代码实现

    MyRunnable类

    public class MyRunnable implements Runnable{
        @Override
        public void run() {
            // 描述任务
            System.out.println(Thread.currentThread().getName() + " ==> 输出666~~");
            try {
                Thread.sleep(Integer.MAX_VALUE);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    测试类

    import java.util.concurrent.*;
    
    public class ThreadPoolTest1 {
        public static void main(String[] args) {
            // 1、通过ThreadPoolExecutor创建一个线程池对象
            ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
                    TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
                    new ThreadPoolExecutor.CallerRunsPolicy());
    
            Runnable target = new MyRunnable();
            pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
            pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
            pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
            pool.execute(target); // 复用前面的核心线程
            pool.execute(target);
            pool.execute(target);
            pool.execute(target); // 任务队列已满
    
            // 到了临时线程的创建时机了
            pool.execute(target);
            pool.execute(target);
    
            // 到了新任务的拒绝时机了!
            pool.execute(target);
    
            // pool.shutdown(); // 等着线程池的任务全部执行完毕后,再关闭线程池
            // pool.shutdownNow(); // 立即关闭线程池!不管任务是否执行完毕!
        }
    }
    
    • 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

    在这里插入图片描述

    线程池处理Callable任务

    ExecutorService的常用方法
    在这里插入图片描述

    代码实现

    MyCallable类

    import java.util.concurrent.Callable;
    
    /**
     * 1、让这个类实现Callable接口
     */
    public class MyCallable implements Callable<String> {
        private int n;
        public MyCallable(int n) {
            this.n = n;
        }
    
        // 2、重写call方法
        @Override
        public String call() throws Exception {
            // 描述线程的任务,返回线程执行返回后的结果
            // 需求:求1-n的和返回
            int sum = 0;
            for (int i = 1; i <= n; i++) {
                sum += i;
            }
            return Thread.currentThread().getName() + "求出了1-" + n + "的和是:" + sum;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    测试类

    import java.util.concurrent.*;
    
    public class ThreadPoolTest2 {
        public static void main(String[] args) throws Exception {
            // 1、通过ThreadPoolExecutor创建一个线程池对象
            ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
                    TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
                    new ThreadPoolExecutor.CallerRunsPolicy());
    
            // 2、使用线程处理Callable任务
            Future<String> f1 = pool.submit(new MyCallable(100));
            Future<String> f2 = pool.submit(new MyCallable(200));
            Future<String> f3 = pool.submit(new MyCallable(300));
            Future<String> f4 = pool.submit(new MyCallable(400));
    
            System.out.println(f1.get());
            System.out.println(f2.get());
            System.out.println(f3.get());
            System.out.println(f4.get());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述

    Executors工具类实现线程池

    什么是Executors

    Executors 是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象

    在这里插入图片描述

    注意:这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象

    Executors使用可能存在的陷阱

    大型并发系统环境中,使用Executors如果不注意可能会出现系统风险。

    在这里插入图片描述

    其它细节知识:并发、并行

    进程

    • 正在运行的程序(软件)就是一个独立的进程
    • 线程是属于进程的,一个进程中可以同时运行很多个线程
    • 进程中的多个线程其实是并发和并行执行的

    并发的含义

    进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。

    并行的理解

    在同一个时刻上,同时有多个线程在被CPU调度执行。

    其它细节知识:线程的生命周期

    什么是线程的生命周期

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

    Java线程的状态

    • Java中总共定义了6种状态
    • 6种状态都定义在Thread类的内部枚举类中
      在这里插入图片描述

    线程的6种状态互相转换

    在这里插入图片描述

    线程的6种状态总结

    在这里插入图片描述

    乐观锁

    前面我们学到的,都是悲观锁,就是一上来就加锁,没有安全感,每次只能一个线程进入访问完毕后,再解锁,线程安全,但是性能较差

    什么是乐观锁

    一开始不上锁,认为是没有问题的,大家一起跑,等要出现线程安全问题的时候才开始控制,线程安全,并且性能较好

    例子

    有1个变量,100个线程,每个线程都要对其加100次

    不加锁

    MyRunnable.java

    public class MyRunnable implements Runnable{
        private int cnt;     // 记录浏览人数
    
        @Override
        public void run() {
            // 加100次
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + " cnt --------> " + (++cnt));
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    Test1.java

    public class Test1 {
        public static void main(String[] args) {
            // 需求:1个变量,100个线程,每个线程对其加100次
            Runnable target = new MyRunnable();
    
            for (int i = 0; i < 100; i++) {
                new Thread(target).start();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    加的顺序不一定是一个一个地加,还有可能出现加不到10000的情况,因为有可能在同一时刻两个线程同时抢到了执行权,对同一个数字进行加1,结果相同,但是少了一次加法

    在这里插入图片描述

    加悲观锁

    MyRunnable.java

    public class MyRunnable implements Runnable{
        private int cnt;     // 记录浏览人数
    
        @Override
        public void run() {
            // 加100次
            for (int i = 0; i < 100; i++) {
                synchronized(this) {
                    System.out.println(Thread.currentThread().getName() + " cnt --------> " + (++cnt));
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    没有问题

    在这里插入图片描述

    加乐观锁

    加乐观锁后,当有两个线程同时抢到执行权时,一个线程在对变量修改完成后,要赋值回去的时候会进行判断,若变量已经被修改了,则这一轮修改作废,重新进行一轮修改

    MyRunnable2.java

    public class MyRunnable2 implements Runnable{
        // 整数修改的乐观锁:用原子类实现
        private AtomicInteger cnt = new AtomicInteger();
    
        @Override
        public void run() {
            // 加100次
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + " cnt --------> " + cnt.incrementAndGet());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    Test2.java

    public class Test2 {
        public static void main(String[] args) {
            // 需求:1个变量,100个线程,每个线程对其加100次
            Runnable target = new MyRunnable2();
    
            for (int i = 0; i < 100; i++) {
                new Thread(target).start();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述

    多线程练习题

    需求描述

    有100份礼品,小红、小明两人同时发送,当剩下的礼品数小于10份的时候则不再送出,利用多线程模拟该过程并将线程的名称打印出来,并最后在控制台分别打印小红、小明分别送出了多少份礼物

    代码

    SendThread.java

    import java.util.List;
    import java.util.Random;
    
    // 发送礼品
    public class SendThread extends Thread {
        private List<String> gift;
        private int cnt;    // 计数
    
        public SendThread(List<String> gift, String name) {
            super(name);
            this.gift = gift;
        }
    
        @Override
        public void run() {
            // 发礼品
            // 注意:锁必须唯一
            Random r = new Random();
            String name = Thread.currentThread().getName();
    
            while (true) {
                synchronized(gift) {
                    if (gift.size() < 10) {
                        break;
                    }
    
                    String rs = gift.remove(r.nextInt(gift.size()));
                    System.out.println(name + "发出了:" + rs);
                    cnt++;
                }
            }
        }
    
        public int getCnt() {
            return cnt;
        }
    }
    
    • 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

    Demo.java

    import java.util.ArrayList;
    import java.util.List;
    import java.util.Random;
    
    public class Demo1 {
        public static void main(String[] args) throws Exception {
            // 创建100份礼品
            List<String> gift = new ArrayList<>();
            String[] names = {"口红", "鲜花", "包包", "剃须刀", "皮带", "手表"};
            Random r = new Random();    // 随机分配礼品名称
            for (int i = 0; i < 100; i++) {
                gift.add(names[r.nextInt(names.length)] + (i+1));
            }
            System.out.println(gift);
    
            // 定义线程类,创建线程对象,去集合中拿礼品给别人
            SendThread xh = new SendThread(gift, "小红");
            xh.start();
    
            SendThread xm = new SendThread(gift, "小明");
            xm.start();
    
            xh.join();
            xm.join();
    
            System.out.println("小红送出了" + xh.getCnt() + "份礼物");
            System.out.println("小明送出了" + xm.getCnt() + "份礼物");
        }
    }
    
    • 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

    在这里插入图片描述

  • 相关阅读:
    递归-深度优先遍历-99. 恢复二叉搜索树
    Node.js 流 Stream【详解】
    AIDL的工作原理与使用示例 跨进程通信 远程方法调用RPC
    MySQL进阶4,常见函数
    RKE vs. RKE2:对比两种 Kubernetes 发行版
    flink技术总结待续
    4.3全局描述符表
    openmmlab教程2-MMCV使用
    【用四道刁钻例题带你理解】数据在内存中存储的方式
    【Java 实战】通过Redis实现购物车功能
  • 原文地址:https://blog.csdn.net/weixin_62511863/article/details/133632610