【JavaLearn】(13)多线程:线程生命周期、线程控制、线程同步、线程通信、线程池、ForkJoin框架

1. 进程和线程

  • 程序:一段静态的代码,是应用程序执行的蓝本

  • 进程:指一种正在运行的程序,有自己的地址空间

    • 动态性(正在运行的程序)
    • 并发性(同时运行)
    • 独立性(QQ 和 微信互不干扰)
  • 并发和并行的区别

    • 并行(parallel):多个 CPU 同时执行多个任务,宏观和微观来看,都是同时执行
    • 并发(concurrency):一个 CPU 同时执行多个任务(采用时间片轮转,A执行一段时间,B再执行一段时间)
  • 线程

    • 进程内部的一个执行单元,它是程序中一个单一的顺序控制流程,又被称为轻量级进程(lightweight process)
    • 如果在一个进程中,同时运行了多个线程,用来完成不同的工作,则被称为多线程
    • 线程特点
      • 轻量级进程
      • 独立调度的基本单位
      • 共享进程资源
      • 并发执行

  • 线程和进程的区别

区别进程线程
根本区别作为资源分配的单位(例:申请30M内存)调度和执行的单位(从30M中划分资源)
开销大:每个进程有自己独立的代码和数据空间小:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器
所处环境操作系统中能同时运行多个任务同一应用程序中有多个顺序流同时执行
分配内存每个进程分配不同的内存区域除了CPU,不会为线程分配资源,使用的都是进程的,线程组只能共享资源
包含关系没有线程的进程可以被看作单线程的,如果一个进程内拥有多个线程,那么就是多个线程同时完成线程是进程的一部分

2. 线程的定义方式

2.1 继承 Thread 类

  • Thread 类常用方法
    • run():线程要执行的任务
    • start():启动线程
    • getName():获取线程名称
    • getPriority():获取优先级
    • Thread.currentThread():得到当前线程
  • 启动 main 方法,自动创建 main 线程

创建一个线程类:(extends Thread

public class TreadDemo extends Thread{
    /**
     * 线程体:线程要执行的任务
     */
    @Override
    public void run() {
        while (true) {
            System.out.println("[A] 线程执行了" + this.getName() + "  " + this.getPriority());
        }
    }
}

创建线程对象、启动线程

// 创建线程对象
Tread treadDemo = new TreadDemo();
treadDemo.setName("设置线程名称"); // 设置线程的名称
treadDemo.setPriority(Thread.MAX_PRIORITY); // 设置线程的优先级


// 启动线程
treadDemo.start();
// treadDemo.run();   此处为普通的方法调用,直接调用 run() 方法

2.2 实现 Runnable 接口

  • 两种方式的比较:
    • 继承 Thread 类:编程简单,但是单继承,无法继承其他类
    • 实现 Runnable 接口:编程稍繁琐,可以再继承其他类,实现其他接口,操作的为同一个任务便于多个线程共享同一个资源

定义一个线程类:(implements Runnable

public class RunnableDemo implements Runnable{
    @Override
    public void run() {
        while (true) {
            System.out.println("[A] 线程执行了" + Thread.currentThread().getName() + "  "
                    + Thread.currentThread().getPriority());
        }
    }
}

创建线程对象、启动线程

// 创建线程对象
Runnable runnable = new RunnableDemo();
Thread thread = new Thread(runnable); // 使用 runnable

// 启动线程
thread.setName("[A] 线程");
thread.setPriority(Thread.NORM_PRIORITY);
thread.start();

// 创建另一个线程对象
Thread thread1 = new Thread(runnable); // 使用同一个 runnable,操作的为同一个任务对象
thread1.setName("[A] 线程 2");

=====》 还可以直接使用匿名内部类的方式

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        while (true) {
            System.out.println("[A] 线程执行了" + Thread.currentThread().getName() + "  "
                               + Thread.currentThread().getPriority());
        }
    }
};

// 创建一个线程
Thread thread = new Thread(runnable); // 使用 runnable
thread.setName("[A] 线程");
thread.setPriority(Thread.NORM_PRIORITY);
thread.start();

// 创建另一个线程
Thread thread1 = new Thread(runnable); // 使用同一个 runnable,操作的为同一个对象
thread1.setName("[A] 线程 2");

2.3 实现 Callable 接口

JDK 1.5后推出

  • 可以有返回值,支持泛型的返回值
  • 可以抛出检查异常
  • 需要借助 FutureTask,获取返回结果等

定义一个线程类:(implements Callable

public class CallableDemo implements Callable<Integer> {
    /**
     * 有返回值,并且可以抛出异常
     * @return
     * @throws Exception
     */
    @Override
    public Integer call() throws Exception {
        if (false) {
            throw new Exception();
        }
        return new Random().nextInt(10);
    }
}

创建线程对象、启动线程

// 创建线程
Callable<Integer> callable = new CallableDemo();
// 需要使用 FutureTask 进行操作
FutureTask<Integer> task = new FutureTask(callable);
// 最终放入的是 FutureTask 对象
Thread thread = new Thread(task);

// 启动线程
thread.start();

// 获取返回值
Integer i = task.get();  // 得不到返回值,就一直等待!!!!!!!!!!
task.get(3, TimeUnit.SECONDS); // 就等待 3 秒,获取不到就报错
System.out.println(i);

3. 线程的生命周期

  • 新生状态:
    • new 创建一个线程对象后,该对象就进入了新生状态
    • 在此时的线程,拥有自己的内存空间,通过 start() 方法进入就绪状态
  • 就绪状态:
    • 此时线程具备了运行条件,但还没分配到CPU
    • 当系统选定一个等待执行的线程后,它就进入了执行状态,称为“CPU调度”
  • 运行状态
    • 执行自己的 run 方法中的代码,直到等待某资源而阻塞,或完成任务而死亡
    • 如果在给定时间片内,没执行结束,就会被系统给换下来回到阻塞状态
  • 阻塞状态
    • 处于运行状态的线程,在某些情况下(执行了sleep方法、等待I/O设备等)将让出CPU 并暂停自己的运行,进入阻塞状态
    • 只有当睡眠时间结束,或资源已获取到,才能进入就绪状态中等待,被系统选中后,从停止的位置继续运行
  • 死亡状态
    • 正常运行的线程,完成了所有的工作
    • 线程被强制性的终止,如通过 stop 方法来终止一个线程【不推荐使用】
    • 线程抛出未捕获的异常

4. 线程控制

可以对线程的生命周期进行干预

基础线程类

public class MyThread extends Thread {
    @Override
    public void run() {
        this.setPriority(6);
        for (int i = 0; i < 100; i++) {
            System.out.println("线程开始执行了:" + this.getName() + "  " + this.getPriority());
        }
    }
}

4.1 join()

阻塞其他线程,等待该线程执行完之后,再执行其他线程

for (int i = 0; i < 100; i++) {
    if (i == 50) {
        Thread thread = new MyThread();
        thread.setName("自定义线程类");
        thread.start();

        // 阻塞主线程,当 自定义线程 执行完,再执行主线程,【在 start() 方法之后】
        thread.join();
    }
    System.out.println("主线程:" + Thread.currentThread().getName());
}

4.2 sleep()

让出 CPU,自身进入阻塞状态

Thread thread = new MyThread();
thread.setName("自定义线程类");
thread.start();

for (int i = 0; i < 100; i++) {
    try {
        Thread.sleep(1000); // 主线程休眠 1 秒,进入【阻塞状态】,时间到后再次进入【就绪状态】
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("主线程:" + Thread.currentThread().getName());
}

4.3 yield()

礼让 CPU,执行一会儿,然后退回到就绪状态,继续争抢CPU(可能会立即抢到,继续执行)

Thread thread = new MyThread();
thread.setName("自定义线程类");
thread.start();

for (int i = 0; i < 100; i++) {
    Thread.yield(); // 主线程进行礼让,执行一会儿后,让出CPU,进入【就绪状态】
    System.out.println("主线程:" + Thread.currentThread().getName());
}

4.4 setDaemon()

守护线程(寄生线程),启动它的线程执行完了,那么它也要停止执行

Thread thread = new MyThread();
thread.setName("自定义线程类");
thread.setDaemon(true); // 启动它的线程执行结束之后,它也停止执行,【在 start() 方法之前】
thread.start();

for (int i = 0; i < 100; i++) {
    System.out.println("主线程:" + Thread.currentThread().getName());
}

4.5 interrupt()

并不是结束了线程,而是修改了线程的状态,需要线程类进行判断isInterrupted()

Thread thread = new MyThread();
thread.setName("自定义线程类");
thread.start();

for (int i = 0; i < 100; i++) {
    System.out.println("主线程:" + Thread.currentThread().getName());
}

thread.interrupt();  // 修改线程的状态,告诉它该结束运行了

线程类修改为:

public class MyThread extends Thread {
    @Override
    public void run() {
        while (this.isInterrupted()) { // 进行判断,是否该结束了
            System.out.println("线程开始执行了:" + this.getName() + "  " + this.getPriority());
        }
    }
}

5. 线程同步

5.1 问题的提出

  • 场景:

    • 多个用户同时操作一个银行账户。每次取款 400 元,取款前先检查余额是否足够,如果不够,放弃取款

      例如 2 个人同时取 400,卡内还有 600 元

  • 分析:

    • 使用多线程解决
    • 开发一个取款线程类,每个用户对应一个线程对象
    • 多个线程共享同一个银行账户,使用 Runnable 方式
  • 思路

    • 创建银行账户类 Account
    • 创建取款线程 AccountRunnable
    • 创建测试类,进行测试

Account 类

public class Account {
    private int balance = 600;

    /**
     * 取款
     * @param money
     */
    public void withDraw(int money) {
        this.balance -= money;
    }

    /**
     * 查看余额
     * @return
     */
    public int getBalance() {
        return balance;
    }
}

AccountRunnable 类

public class AccountRunnable implements Runnable {
    private Account account = new Account();

    /**
     * 取款的步骤
     */
    @Override
    public void run() {
        // 在这个流程中,需要保证【有一个人进入了,另一个人就无法进入】
        // ======================== 取款开始 ===================================
        if (account.getBalance() >= 400) {
            try {
                Thread.sleep(10); // 需要特别注意的地方
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.withDraw(400);
            System.out.println(Thread.currentThread().getName() + " 取款成功,余额:" + account.getBalance());
        } else {
            System.out.println(Thread.currentThread().getName() + " 取款失败,余额:" + account.getBalance());
        }
        // ======================== 取款结束 ===================================
    }
}

TestAccount 类

public static void main(String[] args) {
    // 创建 Runnable 对象
    Runnable runnable = new AccountRunnable();
    
    Thread user1 = new Thread(runnable, "张三");
    user1.start();

    Thread user2 = new Thread(runnable, "张三妻子");
    user2.start();
}

可能出现的结果:

5.2 同步代码块

针对上面的问题,第一种解决方案:使用同步代码块

synchronized (account) { // 使用共享资源作为锁
    
    // 在开始取款之前,先【加上一把锁】,保证【有一个人进入了,另一个人就无法进入】
    // ======================== 取款开始 ===================================
    if (account.getBalance() >= 400) {
        try {
            Thread.sleep(10); // 需要特别注意的地方
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        account.withDraw(400);
        System.out.println(Thread.currentThread().getName() + " 取款成功,余额:" + account.getBalance());
    } else {
        System.out.println(Thread.currentThread().getName() + " 取款失败,余额:" + account.getBalance());
    }
    // ======================== 取款结束 ===================================
}

5.2.1 同步监视器(锁子)

synchronized (同步监视器) { 同步代码块 }

  • 必须是引用数据类型,不能是基本数据类型
  • 在同步代码块中,可以改变它的值,但是不能改变其引用建议使用 final 修饰同步监视器
  • 一般使用共享资源作同步监视器即可
  • 也可创建一个专门的同步监视器,没有任何业务意义
  • 尽量不要使用 String 和包装类(Integer)作同步监视器

5.2.2 执行过程

  • 第一个线程,来到同步代码块,发现同步监视器为 open 状态,需要改为 close,然后执行其中的代码
  • 第一个线程,在执行过程中,发生了线程切换(阻塞),第一个线程就失去了 cpu,但是没有开锁 open
  • 第二个线程,获取了cpu,来到同步代码块,发现同步监视器为 close 状态,无法执行其中的代码,也进入阻塞状态
  • 第一个线程,再次获取 cpu,接着执行后续的代码;执行完成后,开锁 open
  • 第二个线程,也再次获取到 cpu,来到同步代码块,发现为 open 状态,重复第一个线程的处理过程(加锁)

注意:同步代码块中,可以发生线程切换,但是后续的线程,由于无法开锁,所以无法执行同步代码块

5.2.3 分析

  • 加上锁之后,安全了,但是效率降低,而且可能出现死锁

  • 多个代码块使用了同一个锁A,锁住一个代码块的同时,也把其他地方使用这把锁A的代码块锁住了

    导致其他线程无法访问这些代码块

    但是没有锁住使用其他锁B的代码块,其他线程可以执行这些代码块

5.3 同步方法

提取需要加锁的代码块,形成一个方法,给方法加锁(不能给 run() 方法加锁

public synchronized void withDraw() { // 【synchronized】 写在方法上
    if (account.getBalance() >= 400) {
        try {
            Thread.sleep(10); // 需要特别注意的地方
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        account.withDraw(400);
        System.out.println(Thread.currentThread().getName() + " 取款成功,余额:" + account.getBalance());
    } else {
        System.out.println(Thread.currentThread().getName() + " 取款失败,余额:" + account.getBalance());
    }
}
  • 非静态同步方法的锁:this(这个类)
    • 会将所有的非静态同步方法都锁住
  • 静态同步方法的锁:类名.class
    • 会将所有的静态同步方法都锁住
  • 同步代码块的效率高

5.4 Lock 锁

JDK 1.5推出的,API级别,可以调用方法接口

5.4.1 使用步骤

  • 先购买一把锁
  • 在需要同步的代码块之前,上锁
  • 代码块执行结束的地方,解锁(需要手动解锁
public class AccountRunnable implements Runnable {
    private Account account = new Account();

    // 购买锁
    private Lock lock = new ReentrantLock(); // Re-entrant-Lock 【可重入锁】

    /**
     * 取款的步骤
     */
    @Override
    public void run() {
        // ============== 上锁 ==============
        lock.lock();
        try {
            if (account.getBalance() >= 400) {
                try {
                    Thread.sleep(10); // 需要特别注意的地方
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                account.withDraw(400);
                System.out.println(Thread.currentThread().getName() + " 取款成功,余额:" + account.getBalance());
            } else {
                System.out.println(Thread.currentThread().getName() + " 取款失败,余额:" + account.getBalance());
            }
        } finally {
            // ============== 手动解锁 ==============
            lock.unlock();
        }
    }
}

5.4.2 可重入锁

public void method1() {
    lock.lock();  // 此处上锁
    try {
        method2(); // 进入 method2 方法
    } finally {
        lock.unlock();
    }
}

public void method2() {
    lock.lock(); // 由于已经上过锁,直接进入即可,但是需要标识一下,比如是第 2 次进入
    try {
        method3();
    } finally {
        lock.unlock();
    }
}

public void method3() {
    lock.lock(); // 第 3 次进入
    try {
        // TODO
    } finally {
        lock.unlock(); // 解第 3 次的锁
    }
}

5.4.3 Lock 和 synchronized 的区别

  • Lock 是显式锁(手动开启和关闭锁),synchronized 是隐式锁,遇到异常自动解锁
  • Lock 只有代码块锁,synchronized 有代码块锁和方法锁
  • Lock 可以对读不加锁,对写加锁,synchronized 不可以
  • Lock 锁可以有多种获取锁的方式,可以从 sleep 的线程中抢到锁,synchronized 不可以
  • Lock 锁性能好,有更好的扩展性

优先使用顺序:Lock ----- 同步代码块 ------- 同步方法

5.5 线程同步练习 – 卖票

多个窗口卖票,一共100张,不允许重复,出现负数等(只是解决了线程同步问题,但可能会出现一个窗口全部卖出票的情况,后期可以引入线程通信)

Lock 方式

public class TicketRunnable implements Runnable {
    // 直接将票的数量写在这里
    private int ticketNum = 200;

    // 创建锁对象
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            // 对卖一次票的过程 【上锁】
            lock.lock();
            try {
                if (ticketNum <= 0) {
                    break;
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "卖出了第T " + ticketNum + "张票");
                ticketNum--;
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        Runnable runnable = new TicketRunnable();

        // 创建 4 个线程,模拟四个窗口
        Thread t1 = new Thread(runnable);
        t1.setName("[1] 窗口");

        Thread t2 = new Thread(runnable);
        t2.setName("[2] 窗口");

        Thread t3 = new Thread(runnable);
        t3.setName("[3] 窗口");

        Thread t4 = new Thread(runnable);
        t4.setName("[4] 窗口");

        // 开始售票
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

注意:对卖一次票的过程加锁,不要对循环加锁,否则就代表在循环开始之前某一线程先加锁,然后该线程进入循环,一直卖票,直到卖光

6. 线程通信

6.1 生产者和消费者

  • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库消费者将仓库中的产品取走
  • 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到产品被消费者取走(告诉消费者)
  • 如果仓库中有产品,则消费者可以取走消费,否则停止消费并等待,直到仓库中再次放入产品(告诉生产者)

image-20210901212707249

分析

  • 线程同步问题,生产者和消费者共享同一个资源(但是进行的操作不一样,一个是生产,一个是消费,账户的问题都是取钱),且生产者和消费者相互依赖,互为条件
  • 生产者:没有生产产品之前,要告诉消费者等待;生产了产品后,要通知消费者进行消费
  • 消费者:消费之后,告诉生产者继续生产

知识前提:(都是Object类的方法)

  • final void wait() 线程一直等待,直到其他线程通知 【让出 CPU,释放锁】 sleep方法只让出CPU
  • void wait(long timeout) 线程等待一段时间唤醒后继续往下执行
  • final void wait(long timeout, int nanos) 线程等待一段时间
  • final void notify() 唤醒一个处于等待状态的线程
  • final void notifyAll() 唤醒同一个对象上所有调用 wait() 方法的线程,优先级别高的线程先执行

6.2 synchronized方式

必须调用同步监视器(锁子)的 wait()、notify()、notifyAll()

Produce 商品类

public class Produce {
    private String name;
    private String color;
    boolean flag = false;  // 商品有无的状态,默认没有

    // getter / setter 方法
    
    // 有参 / 无参 构造方法,toString()
}

ProduceRunnable 生产者类

public class ProduceRunnable implements Runnable {
    // 此处不能直接 new,不然和消费者操作的不是同一件商品
    private Produce produce;

    // 让消费者获取的生产的对象,保证是同一个【set方式】
    public void setProduce(Produce produce) {
        this.produce = produce;
    }

    @Override
    public void run() {
        for (int i = 0; ; i++) {
            synchronized (produce) {
                // 1.如果已经有商品了,就等待
                if (produce.flag) {
                    try {
                        produce.wait();  // 只释放CPU,同时释放锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                // 2. 生产商品并输出
                if (i % 2 == 0) {
                    produce.setName("馒头");
                    try {
                        Thread.sleep(10);  // 只释放CPU
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    produce.setColor("白色");
                } else {
                    produce.setName("玉米饼");
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    produce.setColor("黄色");
                }
                System.out.println("【生产】了一个 " + produce.getColor() + " 的 " + produce.getName());

                // 3. 改变商品有无状态:有
                produce.flag = true;

                // 4. 通知消费
                produce.notify();  //【随机唤醒一个线程】
            }
        }
    }
}

ConsumeRunnable 消费者类

public class ConsumeRunnable implements Runnable {
    // 此处不能直接 new,不然不是生产者生产的物品
    private Produce produce;

    // 让消费者获取生产的对象,保证是同一个【construct方式】
    public ConsumeRunnable(Produce produce) {
        this.produce = produce;
    }

    @Override
    public void run() {
        while (true) {
            // 对生产者加锁后,也必须对消费者加锁,并且为同一把锁!!!!!!
            synchronized (produce) {
                // 1. 如果没有商品,就等待
                if (!produce.flag) {
                    try {
                        produce.wait();   // 让出CPU,同时释放锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                // 2. 有商品,进行消费
                System.out.println("【消费】了一个 " + produce.getColor() + " 的 " + produce.getName());

                // 3. 改变商品有无状态:无
                produce.flag = false;

                // 4. 通知生产者生产
                produce.notifyAll();  //【唤醒所有等待的线程】
            }
        }
    }
}

Test 测试类

public class Test {
    public static void main(String[] args) {
        // 保证生产和消费的为同一个商品,所以在此处new一个,作为参数传递给生产者和消费者
        Produce produce = new Produce();

        // 生产者生产商品
        ProduceRunnable runnable1 = new ProduceRunnable();
        runnable1.setProduce(produce); // set方法
        Thread t1 = new Thread(runnable1, "生产者");
        t1.start();

        // 消费者进行消费
        Runnable runnable2 = new ConsumeRunnable(produce); // 直接构造方法
        Thread t2 = new Thread(runnable2, "消费者");
        t2.start();
    }
}

问题

  1. 需要定义一个商品类 ======》 生产者生产商品,消费者消费商品
  2. 需要保证生产者生产的,和消费者消费的,是同一件商品 (在测试类中new商品,分别传给生产者和消费者)
  3. 实现线程同步 =====》 避免出现白色的玉米饼,黄色的馒头
  4. 最终实现线程通信 =====》 使用从 Object 类继承的 wait(),notify(),必须用同步监视器的

6.3 线程通信的细节

  • 进行线程通信,必须要使用同一个同步监视器,必须调用该同步监视器的 wait()、notify()、notifyAll()

  • 线程通信的三个方法(都是由同一个同步监视器操作的)

    • wait() 等待:在其他线程未进行唤醒 notify() 【此对象】之前,一直等待,唤醒后继续执行代码
    • wait(time) 一段时间等待:在其他线程未进行唤醒 notify() 或等待时间未到之前,等待
    • notify() 随机唤醒一个:任意的唤醒一个
    • notifyAll() 唤醒所有:唤醒所有的线程,优先级高的先执行
  • 完整的线程生命周期

    • 同步阻塞(锁池队列):没有获取同步监视器(没有拿到锁)的线程的队列
    • 等待阻塞(等待队列):被调用了 wait() 后释放锁,然后进入该队列

    image-20210903222420162

  • sleep() 和 wait() 的区别

    • sleep() :让出CPU,进入阻塞,但不会释放锁,进入阻塞状态
    • wait():让出CPU,进入阻塞,同时会释放锁进入等待队列,只能在同步中使用

6.4 同步方法方式

需要将代码提炼成方法,但是又需要是同一把锁,所以就需要将业务放到 Produce 类

Produce 类

public class Produce {
    private String name;
    private String color;
    boolean flag = false;  // 商品有无的状态,默认没有

    // getter / setter 方法
    
    // 有参 / 无参 构造方法,toString()
    
    // 生产商品
    public synchronized void produce(int i) { // 【锁是this】!!!!!
        // 1.如果已经有商品了,就等待
        if (this.flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 2. 生产商品并输出
        if (i % 2 == 0) {
            this.setName("馒头");
            try {
                Thread.sleep(10); // 只释放CPU
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.setColor("白色");
        } else {
            this.setName("玉米饼");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.setColor("黄色");
        }
        System.out.println("【生产】了一个 " + this.getColor() + " 的 " + this.getName());

        // 3. 改变商品有无状态:有
        this.flag = true;

        // 4. 通知消费
        this.notify();  //【随机唤醒一个线程】
    }

    
    public synchronized void consume() { // 【this 可以省略】
        // 1. 如果没有商品,就等待
        if (!flag) {
            try {
                wait();   // 让出CPU,同时释放锁
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 2. 有商品,进行消费
        System.out.println("【消费】了一个 " + color + " 的 " + name);

        // 3. 改变商品有无状态:无
        flag = false;

        // 4. 通知生产者生产
        notifyAll();  //【唤醒所有等待的线程】
    }
}

6.5 Lock锁

同步代码块和同步方法,生产者和消费者都在一个等待队列中,会存在本想唤醒一个生产者,却唤醒了一个消费者的问题

======》 使用 Lock 锁 + Condition类 解决

  • 创建各自的等待队列(可以有多个等待队列,但只有一个锁池状态)

    Condition produceCondition = lock.newCondition();   //【生产者的队列】
    
  • 线程等待的方法 await()

    produceCondition.await();  // 【进入生产者队列】
    
  • 线程唤醒的方法 signal()

    produceCondition.await();  // 【进入生产者队列】   
    

Produce 类

public class Produce {
    private String name;
    private String color;
    boolean flag = false; // 商品有无的状态,默认没有

    private Lock lock = new ReentrantLock();
    // 创建【生产者】的等待队列
    private Condition produceCon = lock.newCondition();
    // 创建【消费者】的等待队列
    private Condition consumeCon = lock.newCondition();

    public void produce(int i) {
        lock.lock();
        try {
            if (this.flag) {
                try {
                    produceCon.await(); //【进入生产者队列】!!! 使用的是 await()
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            if (i % 2 == 0) {
                this.setName("馒头");
                try {
                    Thread.sleep(10); // 只释放CPU
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.setColor("白色");
            } else {
                this.setName("玉米饼");
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.setColor("黄色");
            }
            System.out.println("【生产】" + this.getColor() + this.getName());

            this.flag = true;

            consumeCon.signal(); // 从【消费者】队列中进行唤醒!!!让其消费
        } finally {
            lock.unlock();
        }
    }

    public void consume() {
        lock.lock();
        try {
            if (!flag) {
                try {
                    consumeCon.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            System.out.println("【消费】" + color + name);

            flag = false;

            produceCon.signalAll(); // 唤醒【生产者】进行生产
        } finally {
            lock.unlock();
        }
    }
}

7. 线程池

7.1 线程池的引入

ThreadPoolExecutor ,对象的创建和销毁是非常消耗时间的,所以可以事先创建好多个线程,放入线程池,使用时直接获取引用,不使用时放回池中(还可以临时的多创建几个线程)

应用场景:需要大量的线程,并且完成任务的时间短;对性能要求苛刻;接受突发性的大量请求

7.2 创建线程池的方法

    • 线程池中【只有一个】线程 newSingleThreadExecutor()

      ExecutorService pool = Executors.newSingleThreadExecutor();
      
    • 线程池中有【固定数量】的线程 newFixedThreadPool(10)

      ExecutorService pool2 = Executors.newFixedThreadPool(10);
      
    • 线程池中线程的数量【可以动态变化】 60s没有任务关闭线程 newCachedThreadPool()

      ExecutorService pool3 = Executors.newCachedThreadPool();
      
    • 用来执行大量的【定时任务】 newScheduledThreadPool(10)

      ExecutorService pool4 = Executors.newScheduledThreadPool(10);
      

    7.3 执行大量的 Runnable命令

    public class ThreadPoolDemo {
        public static void main(String[] args) {
            // 1.创建一个线程池
            // 串行执行,一个接一个  【耗时20s】
            ExecutorService pool = Executors.newSingleThreadExecutor();   // 池中1个线程
            // 10个线程随机选一个任务执行  【耗时2秒】
            ExecutorService pool2 = Executors.newFixedThreadPool(10);     // 池中10个线程
            // 20个任务,创建20个线程 【耗时1秒】
            ExecutorService pool3 = Executors.newCachedThreadPool();      // 根据任务动态变化
            // ExecutorService pool4 = Executors.newScheduledThreadPool(10); // 执行定时任务
    
            // 2.使用线程池,执行大量的 Runnable 命令
            for (int i = 0; i < 20; i++) {
                Runnable runnable = new MyRunnable(i);
                pool.execute(runnable);   // new Thread(runnable).start();
            }
    
            // 3.关闭线程池
            pool.shutdown();
        }
    }
    
    class MyRunnable implements Runnable {
        private int i;
    
        public MyRunnable(int i) {
            this.i = i;
        }
    
        @Override
        public void run() {
            try {
                Thread.sleep(1000); // 每个任务耗时 1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i + " 任务开始执行");
            System.out.println(i + " 任务结束");
        }
    }
    

    7.4 执行大量的 Callable任务

    • 使用 Future 获取返回值
    • Future 的 get() 方法,获取不到返回值时,就一直等待,可以先将 future 放到 list中,之后再获取
    public class ThreadPoolCallable {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            // 创建线程池
            ExecutorService pool = Executors.newCachedThreadPool();
    
            // 暂存结果
            List<Future> list = new ArrayList<>();
            
            // 使用线程池执行大量的 Callable任务
            for (int i = 0; i < 20; i++) {
                Callable task = new Callable(){ // 匿名内部类的方式
                    @Override
                    public Object call() throws Exception {
                        Thread.sleep(1000);
                        return new Random().nextInt(10);
                    }
                };
                
                // 利用 Future 获取返回值
                Future<Integer> future = pool.submit(task); // 调用线程池的 submit方法!!!
                
                // Integer res = future.get();    // 直接 get 需要一直等待获取到结果才返回
                // 可以将结果先放入 List,之后再获取
                list.add(future);
            }
    
            for (Future future : list) {
                System.out.println(future.get());
            }
    
            // 关闭线程池
            pool.shutdown();
        }
    }
    

    7.5 线程池API

    在这里插入图片描述

    image-20210904213404448

    创建线程池时,最终调用的方法

    image-20210904213844477

    • corePoolSize:核心池的大小(正式工的人数)
      • 默认情况下创建了线程池,线程数为0,新任务来时,就会创建一个线程去执行任务
      • 当池中线程数达到 corePollSize 后,就不再创建,把新到的任务放到队列中等待
    • maximumPoolSize:最大线程数(正式工+临时工的人数)
      • 池中可以存放的最大线程数量
    • keepAliveTime + unit:线程存活时间(临时工可以存在的时间)
    • BlockingQueue:阻塞队列
      • 线程都在用时,有新任务进来了,排队等待的地方 线程池只有一个等待队列
    • ThreadFactory:线程工厂
      • 池子中,创建线程的工厂
    • RejectedExecutionHandler:拒绝策略
      • 线程全部在用,等待队列也已经排满,现在来了新的任务,如何对待?
        • CallerRunsPolicy:新创建一个线程来执行这个任务(不属于该线程池)
        • DiscardOldestPolicy:把等待时间最长的那个舍弃,运行新的任务
        • DiscardPolicy:什么也不做
        • AbortPolicy:Java默认,抛出异常

    8. ForkJoin 框架

    JAVA7提供,将一个大的任务,分割成若干个小任务,然后执行求值等,最终汇总每个小任务结果,得到大任务结果

    image-20210905113317598

    8.1 工作窃取算法(work-stealing)

    一个大任务拆分成多个小任务,为减少线程间竞争,把这些子任务分别放到不同的队列有多个等待队列)中,每个队列都有单独的线程(线程都在线程池中)来执行队列中的任务,线程和队列一一对应

    但是会出现一种情况:A处理完了自己队列中的任务,B的队列中还有很多任务要处理

    ==》 A可以选择闲着,不去帮忙

    ==》也可以去帮忙,但是B是从队列头部进行处理的,所以为了不产生竞争A从队列尾部拿任务进行处理(双端队列

    image-20210905114416611

    优点:利用了线程进行并行计算,减少了线程间的竞争

    缺点:如果双端队列中只有一个任务,线程会存在竞争;消耗了更多的系统资源(会创建多个线程和双端队列)

    8.2 主要类

    拥有共同的父类:

    在这里插入图片描述

    • ForkJoinWorkerThread

      ForkJoinPool 线程池中的一个执行任务的线程

    • ForkJoinTask

      用来创建一个 ForkJoin 任务,一般继承它的子类

      • RecursiveAction:没有返回结果的任务
      • RecursiveTask:有返回结果的任务
    • ForkJoinPool

      ForkJoinPool 任务需要通过 ForkJoinPool 来执行

    8.3 ForkJoin 实例

    求1到100的和

    // 1.使用循环求和【相当于4核的cpu,只有1个核在工作】
    int n = 100;
    long sum = 0;
    for (int i = 0; i <= n; i++) {
        sum += i;
    }
    System.out.println(sum);
    
    // 2.使用 ForkJoin 框架求和
    public class SumTask extends RecursiveTask<Long> {
        int start;
        int end;
        int step = 10; // 每10个数相加 为一个任务
    
        public SumTask(int start, int end) {
            this.start = start;
            this.end = end;
        }
    
        @Override
        protected Long compute() {
            // 1. 定义一个结果变量
            long sum = 0;
    
            // 2. 计算结果
            if (end - start <= step) { // 10个数相加的小任务
                for (int i = start; i <= end; i++) {
                    sum += i;
                }
            } else { // 分解大任务为小任务
                // 分解任务
                int mid = (start + end) / 2;
                SumTask leftTask = new SumTask(start, mid);
                SumTask rightTask = new SumTask(mid + 1, end);
    
                // 继续分解
                leftTask.fork(); // 【类似于递归】
                rightTask.fork();
    
                // 结果求和
                long leftSum = leftTask.join();
                long rightSum = rightTask.join();
                sum = leftSum + rightSum;
            }
    
            // 3. 返回结果
            return sum;
        }
    }
    
    // ========================== 测试结果 ===============================
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        int n = 100;
        // 2.1 创建一个 ForkJoin 线程池
        ForkJoinPool pool = new ForkJoinPool();
    
        // 2.2 给出一个求和的任务
        RecursiveTask task = new SumTask(1, n);
    
        // 2.3 将任务交给线程池(线程池会分解任务,并合并结果)
        Future<Long> future = pool.submit(task);
    
        // 2.4 获取结果并输出
        Long res = future.get();
        System.out.println(res);
    
        // 2.5 关闭线程池
        pool.shutdown();
    }
    
    
    • 0
      点赞
    • 1
      收藏
      觉得还不错? 一键收藏
    • 0
      评论
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值