• 25-多线程


    多线程

    线程(Thread)是一个程序内部的一条执行流程。
    程序中如果有一条执行流程,那这个程序就是单线程的程序
    多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)。
    再例如:消息通信、淘宝、京东系统都离不开多线程技术

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

    Java是通过java.lang.Thread 类的对象来代表线程的。
    有两种方法可以创建新的执行线程。 一种是将类声明为Thread的子类。 此子类应覆盖类Threadrun方法。 然后可以分配和启动子类的实例。
    run方法是我们这个线程做什么事情

         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

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

    定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
    创建MyThread类的对象
    调用线程对象的start()方法启动线程(启动后还是执行run方法的)
    方式一优缺点:
    优点:编码简单
    缺点:线程类已经继承 Thread,无法继承其他类,不利于功能的扩展。(java 可以嵌套基层,但不能同时继承多个类,一个类只能有一个父类)

    // 子类的线程
    threadClass tc = new threadClass();
    tc.start();
    // 主函数的线程
    for (int i = 0; i < 5; i++) {
    	System.out.println("main thread is " + i);
    }
    
    public class Mythead extends Thread {
            @Override
            public void run(){
                for (int i = 0; i <5 ; i++) {
                    System.out.println("my thread is "+ i);
                }
            }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    main本身也是一个线程
    线程是同时执行的,所以这个执行语句输出可能是随机的, 并不是俺早顺序输出的。

    my thread is 0
    my thread is 1
    main thread is 0
    main thread is 1
    main thread is 2
    main thread is 3
    main thread is 4
    my thread is 2
    my thread is 3
    my thread is 4
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    注意事项

    1、启动线程必须是调用start方法,不是调用run方法。
    直接调用 run 方法会当成普通方法执行(当成一个类中的内部方法来执行语句),此时相当于还是单线程执行。
    只有调用start方法才是启动一个新的线程执行。
    2、不要把主线程任务放在启动子线程之前。
    这样主线程一直是先跑完的,相当于是一个单线程的效果了。

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

    1. 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
    2. 创建MyRunnable任务对象
    3. 把MyRunnable任务对象交给Thread处理。
    4. 调用线程对象的start()方法启动线程
    Thread类提供的构造器说明
    public Thread(Runnable target)封装Runnable对象成为线程对象

    方式二的优缺点

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

    举例代码

    public static void main(String[] args) {
        RunnableObject ro = new RunnableObject(123);
        new Thread(ro).start();
        for (int i = 0; i <7 ; i++) {
            System.out.println("主 thread is "+ i);
        }
    }
    
    //这个类和别的类几乎没什么区别,我们也可以正常的定义成员变量什么的
    public class RunnableObject implements Runnable{
        long minRunObj;
        public RunnableObject() {
        }
        public RunnableObject(long minRunObj) {
            this.minRunObj = minRunObj;
        }
        @Override
        public void run() {
            for (int i = 0; i <7 ; i++) {
                System.out.println("我的 thread is "+ 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
    创建方式二的匿名内部类的方式写法

    如果这个类我们只使用一次的话, 我们就可以使用这个匿名内部类, 匿名内部类是实现这个接口和实现这个抽象类的一种方法, 我们可以直接在这个函数传递的参数中用匿名内部类来代替实际的参数, 比如下面的这种, 这个 new Runnable 本质上就是一个接口. 我们实现这个接口就是实现了这个匿名内部类.

    new Thread(new Runnable() {
    	@Override
    	public void run() {
    		for (int i = 0; i <7 ; i++) {
    			System.out.println("我的 thread is "+ i);
    		}
    	}
    }).start();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    创建方式三实现Callable接口

    前面两种创建方式都存在的一个问题
    假如线程执行完毕后有一些数据需要返回,他们重写的run方法均不能直接返回结果。
    JDK 5.0提供了Callable接口和FutureTask类来实现(多线程的第三种创建方式)。
    这种方式最大的优点:可以返回线程执行完毕后的结果。

    1. 创建任务对象
      • 定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
      • 把Callable类型的对象封装成FutureTask(线程任务对象)
    2. 把线程任务对象交给Thread对象。
    3. 调用Thread对象的start方法启动线程。
    4. 线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。
      FutureTask的API
    FutureTask提供的构造器说明
    public FutureTask<>(Callable call)把Callable对象封装成FutureTask对象。
    FutureTask提供的方法说明
    public V get() throws Exception获取线程执行call方法返回的结果。

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

    举例
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            //创建对象
            callableObject co = new callableObject(100);
            //是一个任务对象,实现了这个runnable接口,可以直接使用
            FutureTask<String> sft = new FutureTask<>(co);
            new Thread(sft).start();
            System.out.println(sft.get());  //这个获取结果的顺序必须要在启动线程之后,否则没有结果
    
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    实现callable接口的对象

    public class callableObject implements Callable<String> {
    
        private int n;
    
        public callableObject(int n) {
            this.n = n;
        }
    
        @Override
        public String call() throws Exception {
            //求1-n的和
            int sum=0;
            for (int i = 0; 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

    需要注意的是这个 callable 是一个泛型的变量, 我们在使用的时候要根据返回值对其确定类型如果我们的 call 函数的返回值是一个 String 类型的变量, 那么我们就要在这个 callable 后面加上这个否则的话我们这个 call 函数的返回值就要设置为object

    三种线程的创建方式的不同点

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

    Thread的常用方法

    Thread提供的常用方法说明
    public void run()线程的任务方法
    public void start()启动线程
    public String getName()获取当前线程的名称,线程名称默认是Thread-索引
    public void setName(String name)为线程设置名称
    public static Thread currentThread()获取当前执行的线程对象
    public static void sleep(long time)让当前执行的线程休眠多少毫秒后,再继续执行
    public final void join()…让调用当前这个方法的线程先执行完!

    其中比较重点的是getName,和setName,以及这个currentThread,sleep,join等方法
    尤其是这个currentThread很重要
    setName
    我们可以通过这个代码来设置名字,我们也可以通过这个构造器中的代码来进行构造名字

            public Mythead(String name) {
                super(name);
            }
    
    • 1
    • 2
    • 3

    我们可以通过这个代码来获取当前执行的线程对象,使我们能够在随机执行的线程中找到当前线程对象

            Thread t3 = Thread.currentThread();
            System.out.println(t3.getName());
            //System.out.println(Thread.currentThread().getName());
    
    • 1
    • 2
    • 3

    sleep 这个可以让这个程序变快,变慢(可以通过这个设置会员和非会员的速度, 会员的话就让其不休眠, 非会员的话就让它每次运行的话休眠 0.3 秒, 这样不太影响体验, 但确实有区别, 哈哈)
    Thread.sleep(5000)
    让这个当前线程休眠5秒
    join 方法
    注意:
    main函数中使用Thread.currentThread().join()会导致主线程(即main线程)等待自己,这会导致程序陷入死锁状态,因为主线程无法等待自己完成。

            Thread t1= new Mythead();
            t1.start();
    		t1.join();
            System.out.println(t1.getName());
    
    • 1
    • 2
    • 3
    • 4

    常见构造器

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

    线程安全问题

    多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。
    线程安全问题发生的原因是什么?
    多个线程,同时访问同一个共享资源,且同时修改该资源。就会出现问题.

    取钱的线程安全问题

    场景:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,如果小明和小红同时来取钱,并且2人各自都在取钱10万元,可能会出现什么问题呢?
    取钱的三步
    1、判断余额是否足够
    2、吐出100000元
    3、更新账户余额
    第一步两个人执行的时候因为多线程不是按照顺序来的,因为我们可能会两个人通过对余额判断成功

    image-20230810112226615

    线程安全问题出现的原因?

    1. 存在多个线程在同时执行
    2. 同时访问一个共享资源
    3. 存在修改该共享资源

    用程序模拟线程安全问题

    主函数

        public static void main(String[] args) throws ExecutionException, InterruptedException {
            account ac = new account(100000);
            myThread mTh = new myThread(ac,90000,"小明");
            FutureTask ft = new FutureTask(mTh);
            myThread mTh2 = new myThread(ac,50000,"小红");
            FutureTask ft2 = new FutureTask(mTh2);
            new Thread(ft).start();
            new Thread(ft2).start();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

        @Override
        public Double call() throws Exception {
            // 判断这个钱是不是还够,如果不够提示,如果够的话,修改余额
            if (ac.getBalance() < balance) {
                // 钱不够了
                System.out.println(name + "取钱的时候,账户余额不足");
            } else {
                // 钱还够
                System.out.println(name + "取钱的时候,账户余额充足");
                ac.setBalance(ac.getBalance() - balance);
                System.out.println(name + "取钱成功,账户余额为" + ac.getBalance());
            }
            return 0.0;
        }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    出现了这个线程冲突

    小明在取钱,余额充足,还有100000.0
    小红在取钱,余额充足,还有100000.0
    小红取完钱后,剩余-40000.0
    小明取完钱后,剩余10000.0
    
    • 1
    • 2
    • 3
    • 4

    线程同步 解决线程安全的问题

    线程同步
    解决线程安全问题的方案。
    线程同步的思想
    让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
    线程同步的原理
    加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。
    加锁:让多个线程实现先后依次访问共享资源,这样就解决了安全问题。

    加锁的几种方法

    同步代码块

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

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

    原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。
    同步锁的注意事项
    对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出 bug。 下面的这种

        public static void main(String[] args) {
            account ac = new account(100000);
            myThread mTh = new myThread(ac,100000,"小明");
            FutureTask ft = new FutureTask(mTh);
            myThread mTh2 = new myThread(ac,100000,"小红");
            FutureTask ft2 = new FutureTask(mTh2);
    
            new Thread(ft).start();
            new Thread(ft2).start();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
        @Override
        public Double call() throws Exception {
            synchronized ("黑马") {
                if (Double.compare(ac.getMoney(),getMoney)<0){
                    System.out.println(this.name+"在取钱,余额不够,只剩"+ac.getMoney());
                }else {
                    System.out.println(name+"在取钱,余额充足,还有"+ac.getMoney());
                    ac.setMoney(ac.getMoney()-getMoney);
                    System.out.println(name+"取完钱后,剩余"+ac.getMoney());
                }
                //返回余额
                return 0.0;
            }
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    因为这个锁"黑马"是固定的,我们不能够用这个唯一的字符串作为锁,因为黑马不仅能够锁着小明和小红,还会锁着别的人
    最好的是用小明和小红有,别人也有但是每一个张卡都不一样
    synchronized (this.getclass())
    静态方法由于是以类为单位来进行访问的,因此我们在使用的时候我们可以通过synchronized (Account.class);来上锁
    锁对象随便选择一个唯一的对象好不好呢?
    不好,会影响其他无关线程的执行。
    锁对象的使用规范
    建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象。
    对于静态方法建议使用字节码(类名.class)对象作为锁对象。

    同步方法

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

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

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

    • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
    • 如果方法是实例方法:同步方法默认用this作为的锁对象。
    • 如果方法是静态方法:同步方法默认用类名. Class 作为的锁对象。
        @Override
        public synchronized Double call() throws Exception {
            if (Double.compare(ac.getMoney(),getMoney)<0){
                System.out.println(this.name+"在取钱,余额不够,只剩"+ac.getMoney());
            }else {
                System.out.println(name+"在取钱,余额充足,还有"+ac.getMoney());
                ac.setMoney(ac.getMoney()-getMoney);
                System.out.println(name+"取完钱后,剩余"+ac.getMoney());
            }
            //返回余额
            return 0.0;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    是同步代码块好还是同步方法好一点?
    范围上:同步代码块锁的范围更小,同步方法锁的范围更大。
    可读性:同步方法更好。

    方式三 lock锁

    Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。
    Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。

    构造器说明
    public ReentrantLock()获得Lock锁的实现类对象

    Lock的常用方法

    方法名称说明
    void lock()获得锁
    void unlock()释放锁
    • Lock实现提供了比使用synchronized方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构,可能具有完全不同的属性,并且支持多个关联的[Condition]对象。

    注意点
    为了防止这个锁解不开,我们一定要进行try finally进行解锁
    手动进行加锁和解锁,lock,unlock
    final我们最好用这个final进行修饰这个lock,防止它被替换

    image-20230810122441126

    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 Account() {
        }
        public Account(String cardId, double money) {
            this.cardId = cardId;
            this.money = money;
        }
    
        // 小明 小红线程同时过来的
        public void drawMoney(double money) {
            // 先搞清楚是谁来取钱?
            String name = Thread.currentThread().getName();
            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
    • 38

    线程通信,了解一下,理解思想就好了

    什么是线程通信?
    当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。
    线程通信的常见模型(生产者与消费者模型)
    生产者线程负责生产数据
    消费者线程负责消费生产者生产的数据。
    注意:生产者生产完数据应该等待自己,通知消费者消费;消费者消费完数据也应该等待自己,再通知生产者生产!

    image-20230810123021243

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

    注意
    上述方法应该使用当前同步锁对象进行调用。
    唤醒放在前面,等待放在后面
    对下面的代码进行分析发现主函数中有五个线程,这五个线程是在不断的进行竞争的,不一定会谁先进行执行
    所以我们必须要考虑到所有的情况,如果吃货执行的时候没有包子,那么就让其等待,唤醒别的线程,然后又有可能会是吃货执行,在来一次循环, 如果是厨师执行, 那么先判断这个有没有包子, 如果有的话就不做了, 继续等待, 唤醒别的线程, 如果没有的话就做, 做完之后唤醒别的, 把自己等待下去.
    总会有一次是厨师线程执行的,因为这个执行是随机的,也不可能每次都不执行啊

    
        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
    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

    线程池

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

    image-20230810132351186

    如何创建线程池

    谁代表线程池?
    JDK 5.0起提供了代表线程池的接口:ExecutorService。
    如何得到线程池对象?
    方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。
    方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。

    ThreadPoolExecutor构造器

    一个函数七个参数类型

    public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory,RejectedExecutionHandler handler) 
    
    • 1

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

    image-20230810154009891
    例如

    //new LinkedBlockingDeque<>() 无限大小
            //new ArrayBlockingQueue<>(4) 限制这个集合的大小
            new ThreadPoolExecutor(3,5,8, 
                    TimeUnit.SECONDS,new LinkedBlockingDeque<>(), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
    
    • 1
    • 2
    • 3
    • 4

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

    常用方法

    方法名称说明
    void execute(Runnable command)执行 Runnable 任务
    Future submit(Callable task)执行 Callable 任务,返回未来任务对象,用于获取线程返回的结果
    void shutdown()等全部任务执行完毕后,再关闭线程池!
    List shutdownNow()立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务

    举例说明

    main 函数

    ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
                    TimeUnit.SECONDS, new LinkedBlockingDeque<>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
            Runnable target = new MyRunnable();
            pool.execute(target);  //线程池会自动创建一个新线程,自动处理这个任务,自动执行的
            pool.execute(target);  //线程池会自动创建一个新线程,自动处理这个任务,自动执行的
            pool.execute(target);  //线程池会自动创建一个新线程,自动处理这个任务,自动执行的
            pool.execute(target);  //复用前面的核心线程,因为前面三个线程已经执行过了,又空出来了
            pool.execute(target);  //复用前面的核心线程
    		
    		pool.shutdown();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    线程代码

    实现的runable接口
        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            System.out.println(name + " is running");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    运行结果

    pool-1-thread-2 is running
    pool-1-thread-1 is running
    pool-1-thread-3 is running
    pool-1-thread-1 is running
    pool-1-thread-2 is running
    pool-1-thread-3 is runnin
    进程已结束,退出代码0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    注意shutdownNow()
    shutdownNow()是不管这个线程池怎么样,只要执行到这里就关闭线程池
    shutdown是等到这个线程任务执行完毕之后才关闭
    实际上我们也不会关闭这个线程池

    临时线程和拒绝任务

            ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
                    TimeUnit.SECONDS,new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
            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);  //拒绝任务
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    核心线程三个,排队的四个,然后临时线程两个,之后在添加就会拒绝任务了
    此时就会抛出异常,提示你这个线程池满了

    Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task thread.pool.MyRunnable@4f3f5b24 rejected from java.util.concurrent.ThreadPoolExecutor@15aeb7ab[Running, pool size = 5, active threads = 5, queued tasks = 4, 
    
    • 1

    拒绝异常

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

    线程池处理callable的处理方法

    方法名称说明
    void execute(Runnable command)执行任务/命令,没有返回值,一般用来执行 Runnable 任务
    Future submit(Callable task)执行任务,返回未来任务对象获取线程结果,一般拿来执行 Callable 任务
    void shutdown()等任务执行完毕后关闭线程池
    List shutdownNow()立刻关闭,停止正在执行的任务,并返回队列中未执行的任务
            ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
                    TimeUnit.SECONDS,new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
    
    
            Future<String> submit1 = pool.submit(new MyCallable(100));
            Future<String> submit2 = pool.submit(new MyCallable(200));
            Future<String> submit3 = pool.submit(new MyCallable(300));
            System.out.println(submit1.get());
            System.out.println(submit2.get());
            System.out.println(submit3.get());
            Future<String> submit4 = pool.submit(new MyCallable(400));
            Future<String> submit5 = pool.submit(new MyCallable(500));
            System.out.println(submit4.get());
            System.out.println(submit5.get());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    pool-1-thread-1执行从1-100的运算结果是10100
    pool-1-thread-2执行从1-200的运算结果是40200
    pool-1-thread-3执行从1-300的运算结果是90300
    pool-1-thread-1执行从1-400的运算结果是160400
    pool-1-thread-3执行从1-500的运算结果是250500
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Executors 工具实现线程池

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

    Executors使用可能存在的陷阱
    大型并发系统环境中使用Executors如果不注意可能会出现系统风险。
    这种Executors创建的会导致这个等待的任务可以无限长

    image-20230810162939695

    核心线程数量到底配置成多少呢
    计算密集型的任务,核心线程数量 = CPU的核数 + 1
    IO密集型的任务: 核心线程数量 = CPU 核数 + 2

    进程

    1. 正在运行的程序就是一个独立的进程
    2. 线程是属于进程的,一个进程中可以同时运行很多个线程。
    3. 进程中的多个线程其实是并发和并行执行的。

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

    线程的生命周期

    线程的生命周期
    也就是线程从生到死的过程中,经历的各种状态及状态转换。
    理解线程这些状态有利于提升并发编程的理解能力。
    Java线程的状态
    Java总共定义了6种状态
    6种状态都定义在Thread类的内部枚举类中。

    public class Thread{
         ...
             public enum State {
             NEW,
             RUNNABLE,
             BLOCKED,
             WAITING,
             TIMED_WAITING,
             TERMINATED;
         }
         ...
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    线程状态说明
    NEW(新建)线程刚被创建,但是并未启动。
    Runnable(可运行)线程已经调用了start(),等待CPU调度
    Blocked(锁阻塞)线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态;。
    Waiting(无限等待)一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒
    Timed Waiting(计时等待)同waiting状态,有几个方法(sleep,wait)有超时参数,调用他们将进入Timed Waiting状态。
    Teminated(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

    image-20230810165200048

  • 相关阅读:
    AI赋能写作:AI大模型高效写作一本通
    网络运维的重要性
    Phoenix的二级索引
    Java 并发之 AQS 详解(上)
    【解决方案】SkeyeVSS综合安防视频技术保障游乐场安全嗨玩体验
    Django(复习篇)
    【晶振专题】案例:晶振供应商提供的晶振匹配测试报告能看出什么?
    apachesolr启动带调试
    「SpringCloud」07 Gateway服务网关
    Python日常办公10大小技巧
  • 原文地址:https://blog.csdn.net/everything_study/article/details/133501115