小黑子—Java从入门到入土过程:第十章 - 多线程

Java系列第十章- 多线程

1. 初识多线程

进程:

进程是程序的基本执行实体
在这里插入图片描述

线程:

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位

举个例子:360
在这里插入图片描述

360这个软件运行之后,它的本身是一个进程,而它的电脑清理、木马查杀、系统修复、优化加速,就可以把其看作是4个线程
在这里插入图片描述
像这样互相独立的、能同时运行的功能比较多的线程,被称为多线程

多线程和单线程的区别
对比单线程:
在这里插入图片描述
程序从头往下依次运行,cpu不会切换到其他代码中去运行,可见单线程效率比较低

多线程的特点:

能同时的去做多件事情,cpu可以在多个程序之间切换,把等待的空闲时间充分利用起来
在这里插入图片描述

小结:

在这里插入图片描述

2. 并发和并行

并发:
在这里插入图片描述
比如人相当于CPU交替行为作为线程,在玩游戏时,想要做其他的事情
在这里插入图片描述

并行
在这里插入图片描述
在这里插入图片描述
电脑CPU:线程的数量就表示电脑能同时运行多少条线程
在这里插入图片描述
举例4线程:如果说计算机当中有4个线程就不用切换
在这里插入图片描述
但是线程越来越多时,4条红线就会在多个线程之间随机进行切换,所以在计算机当中并发和并行就可能是同时发生的
在这里插入图片描述
小结:
在这里插入图片描述

3. 多线程的实现方式

在这里插入图片描述

3.1 一:继承Thread类方式实现

在这里插入图片描述

线程是程序中的执行线程。Java虚拟机允许应用程序并发地运行多个线程
每一个Thread类都是一个并发的线程
创建新执行线程有两种方法。
一种方法是将类声明为Thread的子类。该子类应重写Thread类的run方法。
接下来可以分配并运行该子类的实例

多线程的第一种启动方式;

  1. 自己定义一个类继承Thread
  2. 重写run方法
  3. 创建子类的对象,并启动线程
MyThread类:
public class MyThread extends Thread{
    @Override
    public void run(){
        //书写线程要执行的代码
        for (int i = 0; i < 20; i++) {
            System.out.println(getName()+"HelloWorld");
        }
    }
}

测试类:
public class Test {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        t1.setName("线程1");
        t2.setName("线程2");

        //MyThread里的start才表示开启线程
        t1.start();
        t2.start();
    }
}

在这里插入图片描述

3.2 二:实现Runnable接口的方式实现

创建线程的另一种方法是声明实现Runable接口的类。该类然后实现run方法。然后可以分配该类的实例,在创建Thread时作为一个参数来传递并启动。

多线程的第二种启动方式:

  1. 自己定义一个类实现Runnable接口
  2. 重写里面的run方法
  3. 创建自己的类的对象
  4. 创建一个Thread类的对象,并开启线程
MyRun类:
public class MyRun implements Runnable{
    @Override
    public void run(){
        //书写线程要执行的代码
        for (int i = 0; i < 20; i++) {
            //获取当前线程的对象Thread.currentThread(),用对象再次调用方法
            System.out.println(Thread.currentThread().getName()+"HelloWorld");
            //这里不能直接调用getName了,因为这个方法是在thread当中的,这个接口没有这个方法的继承关系
            //刚刚的MyThread是在子类里面调用父类里面的方法,所以没问题

        }
    }
}


测试类:
public class Test {
    public static void main(String[] args) {
        //创建MyRund对象
        MyRun mr = new MyRun();
        //创建线程对象
        Thread t1 = new Thread(mr);
        t1.setName("线程1");

        Thread t2 = new Thread(mr);
        t2.setName("线程2");

        t1.start();
        t2.start();
    }
}

在这里插入图片描述

3.3 三:利用Callable接口和Future接口方式实现

特点:可以获取到多线程运行的结果

  1. 创建一个类MyCallable实现Callable接口
    在这里插入图片描述
    这个接口是有泛型的,表示结果。如果说想要开启一个类型让其求1~100的整数和,那么其结果也肯定是一个整数类型,那么此时泛型就可以写Integer

  2. 重写call(是由返回值的,表示多线程运行的结果)

  3. 创建MyCallable的对象(表示多线程要执行的任务)

  4. 创建FutureTask的对象(作用管理多线程运行的结果)

  5. 创建Thread类的对象,并启动(表示线程)

MyCallable类:
public class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            sum = sum + i;
        }
        return sum;
    }
}

测试类:
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable mc = new MyCallable();
        //创建`FutureTask`的对象(作用管理多线程运行的结果)
        FutureTask<Integer> ft = new FutureTask<>(mc);//相当于yongFutureTask对象管理 mc这个对象的结果
        //创建线程的对象
        Thread t1 = new Thread(ft);
        //启动线程
        t1.start();

        //获取多线程的运行结果
        Integer res = ft.get();
        System.out.println(res);
    }

多线程三种实现方式对比:

对比优点缺点
继承Thread类编程比较简单,可以直接使用Thread类中的方法可以拓展性差,不能再继承其他类,无法获得多线程的结果
实现Runable接口拓展性强,实现该接口的同时还可以继承其他的类,可以获得多线程的结果编程相对复杂,不能直接使用Thread类中的方法,无法获得多线程的结果
实现Callable接口拓展性强,实现该接口的同时还可以继承其他的类编程相对复杂,不能直接使用Thread类中的方法

4. 多线程中常见的成员方法

在这里插入图片描述

4.1 线程名字

细节:

  • 如果没有给线程设置名字,线程也是有默认的名字的 格式:Thread-X(X序号,从0开始的) Thread构造方法也可以设置Thread的名字

  • 如果要给线程设置名字,可以用set方法进设置,也可以构造方法设置

在这里插入图片描述

MyThread类:
public class MyThread extends Thread{
    //之前通过setName在父类添加名字,而在测试类中在子类对象MyThread进入写入名字报错,是因为子类不能调用父类的构造方法
    //构造方法是不能继承的,如果说子类想要使用父类的构造,就要自己去写一个新的然后利用super去调用父类的构造


    public MyThread() {
    }

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(getName()+"@"+i);
        }
    }
}

测试类:
    public static void main(String[] args) {
        MyThread mt1 = new MyThread("瓜皮");
        MyThread mt2 = new MyThread("麻瓜");

        //开启线程
        mt1.start();
        mt2.start();
    }

4.2 Thread.currentThread 获取当前线程的对象

细节:

  1. 当JVM虚拟机启动之后,会自动的启动多条线程
  2. 其中有一条线程就叫做main线程
  3. 他的作用就是去调用main方法,并执行里面的代码
  4. 在以前,我们写的所有代码,其实都是运行main线程当中的
测试类:
    public static void main(String[] args) {
        //哪条线程执行到这方法,此时获取的就是哪条线程的对象
        Thread t = Thread.currentThread();
        String name = t.getName();
        System.out.println(name);
    }

在这里插入图片描述

4.3 sleep 让线程休眠指定的时间

关于sleep细节:

  1. 哪条线程执行到这个方阿飞,那么哪条线程就会在这里停留对应的时间
  2. 方法的参数:就表示睡眠的时间,单位毫秒 1秒等于1000毫秒
  3. 当时间到了以后,线程会自动的醒来,继续执行下面的其他代码
    public static void main(String[] args) throws InterruptedException {
        System.out.println("aaaaaaaaa");
        Thread.sleep(5000);//稍等5秒钟,才打印下方
        System.out.println("66666666666");
    }

利用这个方法,在开启线程的时候,可以设置执行的时间
例如:
在这里插入图片描述

4.4 线程的优先级

线程调度:
抢占式调度:

抢夺cpu的执行权(就是个随机,谁抢到算谁的,时间长短也随机),重点掌握其随机性

非抢占式调度:

表示所有的线程轮流的调度,执行的时间也是差不多的

java中是采取抢占式调度
而优先级越大,那么抢到cpu的概率就越大
在这里插入图片描述

 MyRunnable类:
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName()+"@"+i);
        }
    }
}

测试类:
    public static void main(String[] args) {
        //创建线程要执行的参数对象
        MyRunnable mr = new MyRunnable();

        //创建线程对象
        Thread t1 = new Thread(mr,"麻瓜");
        Thread t2 = new Thread(mr,"愣头青");

        System.out.println(t1.getPriority());//默认的5
        System.out.println(t2.getPriority());//默认的5
        System.out.println(Thread.currentThread().getPriority());//默认的5

        t1.setPriority(1);
        t2.setPriority(10);
        //权重越高只是代表抢到cpu先执行完毕的概率越高,并不是代表一定

        t1.start();
        t2.start();

    }

在这里插入图片描述

4.5 setDaemon 守护线程(备胎线程)

细节:

当其他的非守护线程执行完毕之后,守护线程会陆续结束(不是直接结束)

通俗易懂:

当女神进程结束了,那么备胎也没有存在的必要了

应用场景:
比如QQ聊天时,聊天窗口:线程1,传输文件:线程2
当聊天窗口关闭之后,那么正在传输的文件就断开了,也就是线程2这个备胎就没有存在的必要了
在这里插入图片描述

MyThread1类:
public class MyThread1 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i <= 5; i++) {
            System.out.println(getName()+"@"+i);
        }
    }
}

MyThread2类:
public class MyThread2 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i <= 10; i++) {
            System.out.println(getName()+"@"+i);
        }
    }
}

测试类:
public class Test1 {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        MyThread2 t2 = new MyThread2();

        t1.setName("女神");
        t2.setName("备胎");

        t2.setDaemon(true);

        t1.start();//非守护线程
        t2.start();//守护线程

    }
}

在这里插入图片描述

4.6 yield 礼让线程

礼让线程 = 出让线程

场景:
两个线程同时启动,线程的执行是随机性的,有可能是你一次我一次这样的交替执行,也有可能是一个刷一下执行很多个、再刷一下执行另外一个

如果不想这样子,想要线程的执行均匀一点,此时就可以用到yield的出让线程
一个打印完成之后,就会出让其当前cpu的执行权,那么此时又回到了重新抢夺的时候,可以显得均匀一点

只是尽可能地均匀,不是绝对,在以后的代码中这个方法很少会用到

MyThread类:
public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i <= 10; i++) {
            System.out.println(getName()+"@"+i);
        }
    }
}

测试类:
   public static void main(String[] args) {
        MyThread mt1 = new MyThread();
        MyThread mt2 = new MyThread();

        mt1.setName("飞机");
        mt2.setName("坦克");

        mt1.start();
        mt2.start();

    }

在这里插入图片描述

4.7 join 插队线程

小细节:join要放在start的后面才能插队
这个方法在以后用的也不多

MyThread类:
public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i <= 10; i++) {
            System.out.println(getName()+"@"+i);
        }
    }
}

测试类:
    public static void main(String[] args) {
        MyThread mt1 = new MyThread();
        mt1.setName("麻瓜");
        mt1.start();

        //执行在main线程当中的
        for (int i = 0; i < 5; i++) {
            System.out.println("main线程"+i);
        }
    }

在这里插入图片描述
现在的需求:把麻瓜这个线程插入到main线程之前,等到麻瓜执行完了,这个main线程再执行

   public static void main(String[] args) throws InterruptedException {
        MyThread mt1 = new MyThread();
        mt1.setName("麻瓜");
        mt1.start();

        mt1.join();

        //z执行在main线程当中的
        for (int i = 0; i < 5; i++) {
            System.out.println("main线程"+i);
        }
    }

在这里插入图片描述

5. 线程的生命周期

在这里插入图片描述
创建线程对象(新建状态,小宝子)----start()----->有执行资格但是没有执行权(就绪状态,不停的抢CPU,)------抢到CPU的执行权------>有执行资格并且有执行权(运行状态,线程运行代码)-----run()执行完毕---->线程死亡,变成垃圾(死亡状态)

如果被其他线程抢走CPU的执行权,那么运行状态的线程会重新返回就绪状态(有执行资格没有执行权)

如果在运行状态时遇到了sleep()或者其他的阻塞式方法,就会变成没有执行资格也没有执行权的阻塞状态(等着呗),阻塞状态结束会重新变成就绪状态去抢进程

在这里插入图片描述

不会立马执行,会有一个抢夺的过程

6. 线程的安全问题

当多个线程操作同一个数据的时候,会出现问题
在这里插入图片描述

在这里插入图片描述
可见窗口1、2、3,它们的卖票都是独立的,都卖同一张,这不是我想要的。需求:卖出票的总数要为100才行

那么要这么办?

加个statci给ticket票,表示所有类的对象都共享。但是这个方案,也有些问题,有的会出现重复的票和超出范围的票

MyThread类:
public class MyThread extends Thread{
    static int ticket = 0;
    @Override
    public void run() {
        while(true){
            if(ticket<100){
                try {
                    Thread.sleep(500);//这个sleep是没有异常抛出的,只能try
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket++;
                System.out.println(getName()+"正在卖第"+ticket+"张票");
            }else {
                break;
            }
        }
    }
}

测试类:
public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        MyThread mt1 = new MyThread();
        MyThread mt2 = new MyThread();
        MyThread mt3 = new MyThread();

        mt1.setName("窗口1");
        mt2.setName("窗口2");
        mt3.setName("窗口3");

        mt1.start();
        mt2.start();
        mt3.start();

    }
}

在这里插入图片描述
在这里插入图片描述
这些问题要这么解决呢?
把操作共享数据的代码锁起来

6.1 同步代码块

买票引发的安全问题:

  • 原因1:线程在执行代码的时候,CPU的执行全随时有可能会被其他线程抢走 线程执行时,有随机性

在这里插入图片描述
在这里插入图片描述

在自增的时候还没来得及打印,cpu的执行权就被线程2抢走了,又到了线程3依次类推,导致自增,然后同时打印出来,就为卖的是同一张票了
在这里插入图片描述

  • 原因2:线程执行的时候有随机性,CPU的执行权有可能会被其他线程抢走
    在这里插入图片描述
    在这里插入图片描述

解决方案:
在这里插入图片描述

所以要把操作共享数据的代码锁起来——synchronized(锁对象){ }
在这里插入图片描述

特点一:锁默认时打开的,有一个线程进去了,锁自动关闭
特点二:里面的代码全部执行完毕,线程出来,锁自动打开

测试类:
public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        MyThread mt1 = new MyThread();
        MyThread mt2 = new MyThread();
        MyThread mt3 = new MyThread();

        mt1.setName("窗口1");
        mt2.setName("窗口2");
        mt3.setName("窗口3");

        mt1.start();
        mt2.start();
        mt3.start();

    }
}

MyThread类:
public class MyThread extends Thread{
    static int ticket = 0;

    //锁对象,一定要是唯一的
    static Object o = new Object();
    @Override
    public void run() {
        while(true){
            //同步代码块
                synchronized (o){
                    if(ticket<100){
                        try {
                            Thread.sleep(1);//这个sleep是没有异常抛出的,只能try
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        ticket++;
                        System.out.println(getName()+"正在卖第"+ticket+"张票");
                    }else {
                        break;
                    }
                }
        }
    }
}

在这里插入图片描述这个有时候会只有一个线程就卖光所有票了,是抢夺cpu的概率问题

有关同步代码快的小细节:

  1. synchronized不能写在循环外面
    不然就是一个线程把全部的票卖完了才出来

  2. synchronized的锁对象一定是唯一的
    一般的锁会写成当前类的字节码文件
    在这里插入图片描述

就不要用this作为锁,比如:
在这里插入图片描述
应该使用当前类的字节码文件对象
在这里插入图片描述

6.2 同步方法

在这里插入图片描述
选中想要的代码,一键变成构造方法ctl+alt+m

MyRunnable类:
public class MyRunnable implements Runnable{
    int ticket = 0;
    @Override
    public void run() {
        //1.循环
        while(true){
            //2.同步代码块(同步方法)
            if (method()) break;
        }
    }

    private boolean method() {
        synchronized (MyRunnable.class){
            //3.判断共享数据是否到了末尾
            if(ticket==100){
                return true;
            }else {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket++;
                System.out.println(Thread.currentThread().getName()+"在卖第"+ticket+"张票");
            }
        }
        return false;
    }
}

测试类:
public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable mr = new MyRunnable();

        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);
        Thread t3 = new Thread(mr);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();

    }
}

在这里插入图片描述
在这里插入图片描述
字符串在拼接的时候除了StringBuilder,还有stringBuffer,其区别:
它们的方法都是一模一样的

  • 当不需要考虑多线程,采用单线程时,用StringBuilder。当时StringBuilder的实例用于多个线程是不安全的,不能同步
  • 当是多线程环境下,需要考虑到数据安全就选择stringBuffer。stringBuffer的每一个成员方法上都有着sychronized

6.3 Lock 锁

以上面的同步代码块为例,当有线程进来执行代码之后,上头的锁是会自动去关闭的;当线程出来之后,锁又会自动打卡。所有这里锁的开关,没办法自行控制
在这里插入图片描述
那么有什么办法去解决这个问题呢?

在这里插入图片描述
案例:
1.
在这里插入图片描述
这样写的话就会打印很多的票,有很多都超出范围,也有很多的重复票

在这里插入图片描述
为什么出现这种情况呢?
因为如果所定义的MyThread被创建了很多次,那么久势必会造成这里的lock锁,它有多个对象。
解决方案:

  • 加个static关键字给锁就行
    在这里插入图片描述

运行完之后,重复的票和超出范围的票无了,但是新的问题又出现了——程序没有停
在这里插入图片描述
为什么呢?
分析:

  1. 假设刚开始是线程1抢到了cpu的执行权,执行到循环执行到锁lock时,相当于把锁给锁上了
  2. 解决进入判断睡觉,睡觉的时候cpu的执行权会被其他线程2、或3给抢走,但是它抢走之后又重新执行到了lock的位置时,这个锁本来就被线程1给锁上了,就无法再次获取锁的对象,被关在外面了
  3. 到线程1醒来之后,继续往下走,释放锁,线程1回到上面又与线程2、3抢夺cpu执行权
  4. 假设还是线程1拿到了cpu的执行权,等到ticket=100时,跳出循环外面,却没有执行循环里面的unlock方法(即没有打开锁)
  5. 所以这样就导致了线程1结束了,它却拿着锁对象出去了,没有把锁打开。因此线程2、3就一直停止在lock的地方,程序就不会停止

解决方案:

采用try等执行,把lock.unlock方法写在finally一定会执行当中

代码:

MyRunnable类:
public class MyRunnable implements Runnable{
    int ticket = 0;
    static Lock lock = new ReentrantLock();
    @Override
    public void run() {
        //1.循环
        while(true){
            //2.同步代码块(同步方法)
//            synchronized (MyRunnable.class){
            lock.lock();
                //3.判断共享数据是否到了末尾
            try {
                if(ticket==100){
                    break;
                }else {
                    Thread.sleep(100);
                    ticket++;
                    System.out.println(Thread.currentThread().getName()+"在卖第"+ticket+"张票");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
//            }
        }
    }
}

测试类:
   public static void main(String[] args) throws InterruptedException {
        MyRunnable mr = new MyRunnable();

        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);
        Thread t3 = new Thread(mr);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();

    }

在这里插入图片描述
在这里插入图片描述

6.4 理解死锁(一个错误)

在程序当中出现了锁的嵌套,外面一个锁,里面一个锁,这就形成了死锁
在这里插入图片描述

比喻:
线程a拿着a锁,线程b拿着b锁,它们都在等着对方先释放锁,所以在这个时候程序就会卡死
在这里插入图片描述
案例:
在这里插入图片描述

public class MyThread extends Thread {
//定义了两把锁
    static Object obja = new Object();
    static Object objb = new Object();
    @Override
    public void run(){
        while(true){
        if ("线程A".equals(getName())){
            synchronized (obja){
                System.out.println("线程A拿到了A锁,准备拿B锁");
                synchronized (objb){
                    System.out.println("线程A拿到了B锁,顺利执行玩一轮");
                }
            }
        }else if ("线程B".equals(getName())){
            synchronized (objb){
                System.out.println("线程B拿到了B锁,准备拿A锁");
                synchronized (obja){
                    System.out.println("线程B拿到了A锁,顺利执行玩一轮");
                }
            }
        }
        }
    }
}

程序卡死
在这里插入图片描述
总的来说:

以后设置锁的时候,千万不用让锁进行嵌套

7. 等待唤醒机制(生产者和消费者)

在这里插入图片描述
一条线程负责生产数据,另一条线程负责生产数据

比方:
桌子上没吃的就是厨师去烧菜
在这里插入图片描述

理想状态就是先是厨师抢到CPU执行权,做了一碗吃的,然后吃货来吃,吃货吃一碗,厨师做一碗

两种情况可能会出现:

1.消费者等待 一开始不是厨师抢到执行权而是吃货抢到,桌子上没东西(桌子不能吃),所以吃货只能等待,那么执行权就会被厨师抢到,做完以后厨师会唤醒在等待的吃货去吃
消费者:

  • 1、判断桌子上是否有食物
  • 2、如果没有就等待

生产者:

  • 1、制作食物
  • 2、把食物放在桌子上
  • 3、叫醒等待的消费者开吃
    在这里插入图片描述

2.生产者等待 第一时间是厨师抢到CPU执行权,然后做好吃的。 第二次还是厨师抢到CPU的执行权,因为桌子上有吃的,所以不会去做吃的,会”喊一嗓子“,进入等待状态,等待吃货抢到CPU执行权,吃掉桌子上的东西以后才会继续让厨师运作起来
消费者:

  • 1、判断桌子上是否有食物
  • 2、如果没有就等待
  • 3、如果有就开吃
  • 4、吃完之后,唤醒厨师继续做

生产者:

  • 1、判断桌子上是否有食物
  • 2、有:等待
  • 3、没有:制作食物
  • 4、把食物放在桌子上
  • 5、叫醒等待的消费者开吃
    在这里插入图片描述

常见方法:
在这里插入图片描述

7.1 消费者代码实现

Desk桌子类:
public class Desk {
    /*
    桌子的作用:控制生产者和消费者的执行
     */
    //判断是否有面条
    //0:没有面条    1:有面条
    public static int foodFlag = 0;

    //总个数
    public static int count =10;

    //锁对象
    public static Object lock = new Object();
}

Foodie消费者类:
public class Foodie extends Thread{

    @Override
    public void run() {
    /*线程四步套路:
    1.循环
    2.同步代码块
    3.判断共享数据是否到了末尾(到了末尾的情况)
    4.没有到达末尾的情况,执行核心逻辑
     */
        while (true){
            synchronized (Desk.lock){
                if(Desk.count==0){
                    //代表面条总个数没有了
                    //当消费者把我库存的10碗面条吃完了,线程就要停止
                    break;
                }else {
                    //先判断桌子上是否有面条
                    if(Desk.foodFlag==0){
                        //如果有,就等待
                        try {
                            Desk.lock.wait();
                            //等待的时候不能直接用wait
                            //让锁对象等待,就是让当前线程同锁对象进行绑定
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }else {
                        //有面条,把吃的总数-1
                        Desk.count--;
                        System.out.println("消费者在吃面条,还能吃"+Desk.count+"碗");

                        //吃完之后,唤醒厨师继续做
                        Desk.lock.notifyAll();

                        //然后修改桌子的状态,表示原本的那一碗吃完了
                        Desk.foodFlag = 0;
                    }
                }
            }
        }

    }
}

7.2 生产者代码实现

Cook类:
public class Cook extends Thread{
    @Override
    public void run() {
        while(true){
            synchronized (Desk.lock){
                if(Desk.count==0){
                    break;
                }else {
                    //厨师判断桌子上是否有食物
                    if(Desk.foodFlag==1){
                        //如果有,就等待
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }else {
                        //如果没有食物,厨师开做
                        System.out.println("厨师做了一碗面条");
                        //修改桌子上的食物状态
                        Desk.foodFlag=1;
                        //叫醒消费者开吃
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}

测试类:
public class ThreadDemo {
    public static void main(String[] args) {
        //创建消费者和生产者线程对象
        Cook c = new Cook();
        Foodie f =new Foodie();

        //给线程设置名字
        c.setName("厨师");
        f.setName("吃货");

        c.start();
        f.start();
    }
}

在这里插入图片描述

7.3 阻塞队列实现等待唤醒机制

什么是阻塞队列?

就好比是连接生产者和消费者之间的管道

厨师做好面条之后就把其放到管道当中,而左边的消费者就可以从管道当中获取面条去吃,管道可以规定最多放多少面条。所以这个管道其实就是阻塞队列
在这里插入图片描述
在这里插入图片描述
需求:利用阻塞队列完成生产者和消费者(等待唤醒机制)的代码

细节:生产者和消费者必须使用同一个阻塞队列

Cook类:
public class Cook extends Thread{
    ArrayBlockingQueue<String> queue;//这次只定义不给值
    //需要在创建对象的时候才给值

    //创建对象的时候把其地址值给传递过来
    public Cook(ArrayBlockingQueue<String> queue) {
        this.queue = queue;//传递过来之后再赋给成员变量这个队列就可以了
    }

    @Override
    public void run() {
        while(true){
            //不断的把面条放进阻塞队列当中
            //其put方法中底层有锁,不用再设置锁
            try {
                queue.put("面条");
                System.out.println("厨师放了一碗面条");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Foodie类:
public class Foodie extends Thread{
    ArrayBlockingQueue<String> queue;//这次只定义不给值
    //需要在创建对象的时候才给值

    //创建对象的时候把其地址值给传递过来
    public Foodie(ArrayBlockingQueue<String> queue) {
        this.queue = queue;//传递过来之后再赋给成员变量这个队列就可以了
    }

    @Override
    public void run() {
        while(true){
            //不断地从阻塞队列中获取面条
            //其take方法中底层有锁,不用再设置锁
            try {
                String food = queue.take();
                System.out.println("消费者吃了"+food);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

测试类:
public class ThreadDemo {
    public static void main(String[] args) {
        //创建阻塞队列中的对象
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);

        //创建线程,把阻塞队列传递过去
        Cook c = new Cook(queue);
        Foodie f = new Foodie(queue);

        c.start();
        f.start();
    }
}

在这里插入图片描述
这竟然出现了重复的,因为queue的put和take底层都有锁,而打印语句其实是定义在锁的外面,所以就会造成控制台当中数据的这种现象。但是它不会对共享数据造成任何影响

8. 线程的状态

在这里插入图片描述

java的虚拟机当中是没有定义运行状态的,理解时添加的

java当中真正定义的其实就是6种状态,是没有定义运行状态的
为什么呢?

当线程抢夺到cpu的执行权时,那么此时虚拟机就会把当前的线程交给操作系统去管理了,虚拟机不管这个,所以就没有定义运行状态
就好比买了不手机,手机坏了交出去微信,就不管了
在这里插入图片描述

在这里插入图片描述

9. 多线程综合练习

9.1 卖电影票

在这里插入图片描述

9.2 送礼品

在这里插入图片描述

9.3 打印奇数数字

在这里插入图片描述

9.4 抢红包

在这里插入图片描述
选用JDK17默认的SDK,才能使用nextDouble
在这里插入图片描述
在这里插入图片描述
不然报错:
在这里插入图片描述

MyThread类:
public class MyThread extends Thread{
   //共享数据
    //100块,分成了3个包
    static double money = 100;
    static int count = 3;
    //最小的中奖金额
    static final double MIN = 0.01;

    @Override
    public void run() {
        //同步代码块
        synchronized(MyThread.class){
            //判断,共享数据是否到了末尾,没有
            if(count==0){
                System.out.println(getName()+"没有抢到票");
            }else {
                //判断,到了末尾
                //定义一个变量,表示中奖的金额
                double prize = 0;
                if(count==1){
                    //表示此时是最后一个红包
                    //就无需随机,剩余所有的钱都是中奖金额
                    prize = money;
                }else {
                    //表示第一次,第二次 (要随机)
                    Random r = new Random();
                    //100 分成3个包
                    //要像到最夸张的情况,第一个红包:99.98 剩下分配0.01 .0.1
                    //100-(3-1)*0.01
                    double bounds = money - (count - 1) * MIN;
                    prize = r.nextDouble(bounds);//nextDouble是JDK17以上才有的
                    if(prize<MIN){
                        prize = MIN;
                    }
                }
                //从money中,去掉当前中奖的金额
                money = money - prize;
                //然后红包的个数-1
                count--;
                System.out.println(getName()+"抢到了"+prize+"元");

            }


        }
    }


}


测试类:
public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        MyThread mt1 = new MyThread();
        MyThread mt2 = new MyThread();
        MyThread mt3 = new MyThread();
        MyThread mt4 = new MyThread();
        MyThread mt5 = new MyThread();

        mt1.setName("麻瓜");
        mt2.setName("里皮");
        mt3.setName("AKM");
        mt4.setName("小老板");
        mt5.setName("愣头青");

        mt1.start();
        mt2.start();
        mt3.start();
        mt4.start();
        mt5.start();

    }
}

在这里插入图片描述

生活实际没有小数点后面这么多位:

public class MyThread extends Thread{
   //共享数据,利用BigDecimal精确运算到我想要的小数点
    //用double小数点太多
    //100块,分成了3个包
    static BigDecimal money = BigDecimal.valueOf(100.00);
    static int count = 3;
    //最小的中奖金额
    static final BigDecimal MIN = BigDecimal.valueOf(0.01);

    @Override
    public void run() {
        //同步代码块
        synchronized(MyThread.class){
            //判断,共享数据是否到了末尾,没有
            if(count==0){
                System.out.println(getName()+"没有抢到票");
            }else {
                //判断,到了末尾
                //定义一个变量,表示中奖的金额
                BigDecimal prize;
                if(count==1){
                    //表示此时是最后一个红包
                    //就无需随机,剩余所有的钱都是中奖金额
                    prize = money;
                }else {
                    //表示第一次,第二次 (要随机)
                    Random r = new Random();
                    //100 分成3个包
                    //要像到最夸张的情况,第一个红包:99.98 剩下分配0.01 .0.1
                    //100-(3-1)*0.01
                    double bounds = money.subtract(BigDecimal.valueOf(count-1).multiply(MIN)).doubleValue();
                    prize = BigDecimal.valueOf(r.nextDouble(bounds));//nextDouble是JDK17以上才有的

                }
                //设置抽中红包,小数点保留两位,四舍五入
                prize=prize.setScale(2, RoundingMode.HALF_UP);
                //从money中,去掉当前中奖的金额
                money = money.subtract(prize);
                //然后红包的个数-1
                count--;
                System.out.println(getName()+"抢到了"+prize+"元");

            }


        }
    }


}

在这里插入图片描述

9.5 抽奖箱抽奖

在这里插入图片描述

MyThread类:
public class MyThread extends Thread{
    //采用集合更方便,不重复元素
    ArrayList<Integer> list;
    //为了防止线程1、2等跑下面方法会重复添加元素进集合当中
    //可以在成员位置去定义一个构造方法,在创建对象的时候把集合传递过来,这样集合也是唯一的

    public MyThread(ArrayList<Integer> list) {
        this.list = list;
    }

    @Override
    public void run() {
        while(true){
            synchronized (MyThread.class){
                if(list.size()==0){//如果集合里的元素被删到没有了就结束
                    break;
                }else {
                    //继续抽奖
                    Collections.shuffle(list);
                    int prize = list.remove(0);
                    System.out.println(getName()+"又产生了"+prize+"的大奖");
                }
                //在锁的外面,让线程平均一点,如果写在锁的里面效果不明显
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

测试类:
public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        //创建奖池
        ArrayList<Integer> list = new ArrayList<>();
        Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);

        //创建线程
        MyThread mt1 = new MyThread(list);
        MyThread mt2 = new MyThread(list);

        mt1.setName("麻瓜");
        mt2.setName("愣头青");
        //抽奖
        mt1.start();
        mt2.start();

    }
}

在这里插入图片描述

9.6 多线程统计并求最大值

在这里插入图片描述
解法一:

public class MyThread extends Thread{
    //采用集合更方便,不重复元素
    ArrayList<Integer> list;
    //为了防止线程1、2等跑下面方法会重复添加元素进集合当中
    //可以在成员位置去定义一个构造方法,在创建对象的时候把集合传递过来,这样集合也是唯一的

    public MyThread(ArrayList<Integer> list) {
        this.list = list;
    }

    //将抽奖结果存入到一个容器当中
    static ArrayList<Integer> list1 = new ArrayList<>();
    static ArrayList<Integer> list2 = new ArrayList<>();

    @Override
    public void run() {
        while(true){
            synchronized (MyThread.class){
                if(list.size()==0){//如果集合里的元素被删到没有了就结束
                    if("抽奖箱1".equals(getName())){
                        System.out.println("抽奖箱1"+list1);
                    }else {
                        System.out.println("抽奖箱2"+list2);
                    }
                    break;
                }else {
                    //继续抽奖
                    Collections.shuffle(list);
                    int prize = list.remove(0);
                    if("抽奖箱1".equals(getName())){
                        list1.add(prize);
                    }else {
                        list2.add(prize);
                    }
                }
                //在锁的外面,让线程平均一点,如果写在锁的里面效果不明显
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

解法二:
如果要创建1000给线程的话,采用一的方案就非常麻烦

所以可以创建一个集合,每次线程进来都放进集合当中,将得奖放进集合当中,然后与获取名字+集合的形式打印出来即可

MyThread类:
public class MyThread extends Thread{
    //采用集合更方便,不重复元素
    ArrayList<Integer> list;
    //为了防止线程1、2等跑下面方法会重复添加元素进集合当中
    //可以在成员位置去定义一个构造方法,在创建对象的时候把集合传递过来,这样集合也是唯一的

    public MyThread(ArrayList<Integer> list) {
        this.list = list;
    }

    //将抽奖结果存入到一个容器当中
    static ArrayList<Integer> list1 = new ArrayList<>();
    static ArrayList<Integer> list2 = new ArrayList<>();

    @Override
    public void run() {
        ArrayList<Integer> boxList = new ArrayList<>();
        while(true){
            synchronized (MyThread.class){
                if(list.size()==0){//如果集合里的元素被删到没有了就结束
                    System.out.println(getName()+boxList);
                    break;
                }else {
                    //继续抽奖
                    Collections.shuffle(list);
                    int prize = list.remove(0);
                    boxList.add(prize);
                }
                //在锁的外面,让线程平均一点,如果写在锁的里面效果不明显
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

测试类:
public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        //创建奖池
        ArrayList<Integer> list = new ArrayList<>();
        Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);

        //创建线程
        MyThread mt1 = new MyThread(list);
        MyThread mt2 = new MyThread(list);

        mt1.setName("抽奖箱1");
        mt2.setName("抽奖箱2");
        //抽奖
        mt1.start();
        mt2.start();

    }
}

在这里插入图片描述
内存图解:
在这里插入图片描述
在这里插入图片描述
每个线程都有自己的集合互不干扰

9.7 多线程之间的比较

在这里插入图片描述

MyCallable类:
public class MyCallable implements Callable<Integer> {
    //采用集合更方便,不重复元素
    ArrayList<Integer> list;
    //为了防止线程1、2等跑下面方法会重复添加元素进集合当中
    //可以在成员位置去定义一个构造方法,在创建对象的时候把集合传递过来,这样集合也是唯一的

    public MyCallable(ArrayList<Integer> list) {
        this.list = list;
    }

    //将抽奖结果存入到一个容器当中
    static ArrayList<Integer> list1 = new ArrayList<>();
    static ArrayList<Integer> list2 = new ArrayList<>();


    @Override
    public Integer call() throws Exception {
        ArrayList<Integer> boxList = new ArrayList<>();
        while(true){
            synchronized (MyCallable.class){
                if(list.size()==0){//如果集合里的元素被删到没有了就结束
                    System.out.println(Thread.currentThread().getName()+boxList);
                    break;
                }else {
                    //继续抽奖
                    Collections.shuffle(list);
                    int prize = list.remove(0);
                    boxList.add(prize);
                }
            }
            Thread.sleep(100);
        }
        if(boxList.size()==0){
            return null;
        }else {
            return Collections.max(boxList);
        }
    }
}


测试类:
public class Test1 {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //创建奖池
        ArrayList<Integer> list = new ArrayList<>();
        Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);

        //创建多线程要运行的参数对象
        MyCallable mc = new MyCallable(list);

        //创建多线程运行结果的管理者对象
        FutureTask<Integer> ft1 = new FutureTask<>(mc);
        FutureTask<Integer> ft2 = new FutureTask<>(mc);

        //创建线程对象
        Thread t1 =new Thread(ft1);
        Thread t2 = new Thread(ft2);

        //设置名字
        t1.setName("抽箱1");
        t2.setName("抽箱2");

        t1.start();
        t2.start();

        System.out.println(ft1.get());
        System.out.println(ft2.get());
    }
}

在这里插入图片描述

10. 线程池

以前写多线程的弊端
弊端1:用到线程的时候就创建
弊端2:用完之后线程消失
浪费了系统的资源
在这里插入图片描述
那么我们就要用一个容器去存放线程——线程池

线程池主要核心原理
1、创建一个池子,池子中是空的。
2、提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子,下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可
3、但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待。
在这里插入图片描述

特殊情况:
在这里插入图片描述
线程池代码实现:
在实际开发当中,线程池一般是不会关闭的,因为服务器是24小时运行的
在这里插入图片描述
线程池工具类:

Executors:线程池的工具类通过调用方法返回不同类型的线程池对象
在这里插入图片描述

IDEA快捷改类名字——shift+F6
1.

 MyRunnable类:
public class MyRunnable implements Runnable{
    @Override
    public void run() {
//        for (int i = 0; i <= 10; i++) {
            System.out.println(Thread.currentThread().getName()+"---");
//        }
    }
}

测试类:
public class Test1 {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //1.获取线程池对象
        ExecutorService pool1 = Executors.newCachedThreadPool();

        //2.提交任务
        //每次提交之前都让main线程睡觉,目的就是让任务感觉执行完毕把线程还回去
        pool1.submit(new MyRunnable());
        Thread.sleep(1000);
        pool1.submit(new MyRunnable());
        Thread.sleep(1000);
        pool1.submit(new MyRunnable());
        Thread.sleep(1000);
        pool1.submit(new MyRunnable());
        Thread.sleep(1000);

//        //3.销毁线程池
//        pool1.shutdown();
    }
}

在这里插入图片描述
2.

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <= 10; i++) {
            System.out.println(Thread.currentThread().getName()+"---"+i);
        }
    }
}

测试类:
public class Test1 {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //1.获取线程池对象
        ExecutorService pool1 = Executors.newFixedThreadPool(3);

        //2.提交任务
        //每次提交之前都让main线程睡觉,目的就是让任务感觉执行完毕把线程还回去
        pool1.submit(new MyRunnable());
        Thread.sleep(1000);
        pool1.submit(new MyRunnable());
        Thread.sleep(1000);
        pool1.submit(new MyRunnable());
        Thread.sleep(1000);
        pool1.submit(new MyRunnable());
        Thread.sleep(1000);

//        //3.销毁线程池
//        pool1.shutdown();
    }
}

在这里插入图片描述

10.1 自定义线程池详细解析

饭店里招员工
来一个顾客临时找一个服务员,进行一对一服务
顾客走了,服务员也就辞退了
但是这个时候引进了正式员工
就算没有顾客,正式员工也不会被辞退
但是顾客过多时还是会引入临时员工进行帮忙

核心元素一:正式员工数量
核心元素二:餐厅最大员工数
核心元素三:临时员工空闲多长时间被辞退(值)
核心元素四:临时员工空闲多长时间被辞退(单位)
核心元素五:排队的客户
核心元素六:从哪里招人
核心元素七:当排队人数过多,超出顾客请下次再来(拒绝服务)
在这里插入图片描述

细节:
什么时候才会去创建临时线程?
核心线程都在忙,而且队伍已经排满了,才会去创建临时线程
在这里插入图片描述

任务在执行的时候,一定是按照提交的顺序来执行的吗?

不是,任务4、5、6还在排队呢,而后提交的7、8已经正在执行了

当线程满负荷时,如果还有其他线程就会触发拒绝策略,舍弃不要

在这里插入图片描述

10.2 自定义线程池的任务拒绝策略

在这里插入图片描述

    public static void main(String[] args) throws IOException {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
          3,//核心线程数量,能小于0
          6,//最大线程数,不能小于0,最大数量>=核心线程数量
          60,//允许空闲线程最大存活世界
                TimeUnit.SECONDS,//时间单位
                new ArrayBlockingQueue<>(3),//任务独立
                Executors.defaultThreadFactory(),//创建线程工厂
                new ThreadPoolExecutor.AbortPolicy()//任务拒绝策略,为什么将其定义位内部类?
                //因为内部类是依赖于外部类而存在的,单独出现没有任何意义,而且内部类的本身又是一个独立的个体
        );
    }

关于拒绝策略是内部类
为什么将其定义位内部类?

因为内部类是依赖于外部类而存在的,单独出现没有任何意义,而且内部类的本身又是一个独立的个体

拒绝策略单独存在没有意义,只有在线程池中才有意义
就像心脏单独存在没有意义,只有在人体中才有意义

线程池小结
1、创建一个空的池子
2、有任务提交时,线程池会创建线程去执行任务,执行完毕归还线程

不断地提交任务,会有以下三个临界点:
1、当线程池满时,再提交任务就会排队
2、当核心线程满,队伍满时,会创建临时线程
3、当核心线程满,队伍满,临时线程满时,会触发任务拒绝策略

10.3 最大并行数

最大并行数和cpu有关
以4核8线程的为例:

4核就相当于cpu有4个大脑,在超线程技术的支持下,就能把原本的四个大脑虚拟成8个,这8个就是线程,最大并行数是8

向java虚拟机返回可用处理器的数目

    public static void main(String[] args) throws IOException {

        int count = Runtime.getRuntime().availableProcessors();
        System.out.println(count);
    }

my电脑线程数为16,故最大并行数就用16来进行计算
在这里插入图片描述

10.4 线程池多大合适

项目可以分成两种

  1. CPU密集型运算(计算多,读取录入少):

最大并行数+1(+1是为了万一前面有个线程出问题了,保证CPU的时钟周期不被浪费。说简单点就是后补,前面出问题,后面就要顶上去)
比如我的线程是16,那么线程池最大就是16+1=17

  1. I/O密集型运算(读取录入多):
    比如当执行业务计算的时候,此时会使用cpu的资源,但是当进行I/O操作的时候或者远程调用rpc操作操作、或者操作数据库的时候,这个时候cpu就闲置下来了。此时就可以利用多线程技术,把闲下来的时间给利用起来

公式:

最大并行数期望CPU利用率总共时间(CPU计算时间+等待时间)/CPU计算时间

在这里插入图片描述
那么cpu的计算时间和等待时间怎么获取呢?
可以用thread dump工具来测试

11. 多线程的额外拓展内容

面试的时候喜欢问该知识点
在这里插入图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值