大数据---javase基础---day10

额外知识点

程序、软件、进程、线程

  • 程序
    为了完成某个任务和功能,选择一种编程语言编写的一组有序的指令的集合(可以说一个功能就是一个程序)
  • 软件
    一个或多个应用程序+相关的素材和资源文件等构成的一个软件系统(可以说是由多个程序组成的系统)
  • 进程
    指一个在内存中运行的应用程序的实例,每个实例都是一个独立的进程,每个进程都有一个独立的内存空间,不同的进程之间相互独立、互不干扰
  • 线程
    线程包含在进程中,是进程中的一个执行单元,是进程中的实际运作单位,负责当前进程中程序的执行,是指在单个程序中同时执行的一组并发执行路径,一个进程中至少有一个线程,可以有多个线程(此时应用程序称为多线程程序),具体数量取决于操作系统和硬件资源的限制
  • 注意:
    (1)一个软件中至少有一个应用程序,应用程序的一次运行就是一个进程,一个进程中至少有多个线程
    (2)一个应用程序的多次运行就是多个进程,比如:打开两次画图
    (3)一个进程中包含多个线程请添加图片描述

并发与并行

  • 并发
    指两个或多个事件在同一时刻发生(同时发生)
    **并发要求:**要不就是有多个CPU,要不就是一个CPU有多个计算核心
  • 并行
    指两个或多个事件在同一个时间段内发生,但在同一个时间段内的同一个时刻只能有一条指令执行。(多个进程的指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果)

线程调度

  • 分时调度
    所有线程轮流拥有CPU使用权,平均分配每个线程占用的CPU时间
  • 抢占式调度
    优先让优先级高的线程使用CPU,若线程的优先级相同,则会随机选择一个。它允许高优先级的进程在任何时候抢占低优先级的进程并立即获得CPU的控制权。如果某个高优先级的进程需要执行,则会立即抢占正在执行的低优先级进程,使其暂停执行,然后将CPU的控制权转移到高优先级的进程上
    注意: 与抢占式调度相对的是非抢占式调度,即一个进程在获得CPU控制权后,一直执行到完成或放弃CPU控制权。在非抢占式调度中,高优先级的进程无法强制中断低优先级的进程,直到它自行放弃CPU控制权或完成执行

创建和启动子线程(两种方式)

继承Thread类(线程类)

java中使用java.lang.Thread类代表线程,所有线程对象都必须是Thread类或其子类的实例
注意: 该创建子线程的方法存在缺陷,若子类本来就有父类,则此时就没办法再继承Thread类来启动多线程了(原因:java类只支持单继承)

继承Thread类来创建并启动单线程步骤

  • 定义Thread类的子类(即继承Thread类)并重写该类的run()方法(原因:开启子线程后,会执行run()方法体内的代码)请添加图片描述
  • 创建Thread类的子类的实例(即创建线程对象)请添加图片描述
  • 调用线程对象的start()方法来启动子线程(注意:开启子线程后会调用run()方法)请添加图片描述
  • 此时即多个线程同时运行请添加图片描述

继承Thread类来创建并启动多线程步骤

由于一个线程对象只能开启一个子线程,所以要开启多个子线程时,你就多创建几个线程对象就可以了
相关代码如图所示
请添加图片描述

实现Runnable接口

通过实现Runnable接口创建并启动单线程步骤

  • 线程类实现接口并实现run()方法请添加图片描述
  • 创建Thread类的子类的实例(即创建线程对象)请添加图片描述
  • 创建Thread对象并将myThreadTwo作为参数传入Thread,以此来调用start方法来开启子线程请添加图片描述
  • 此时即多个线程同时运行请添加图片描述

实现Runnable后通过匿名内部类或Lambda表达式来启动单线程

package at.guigu.day10;

public class TestMyThreadTwo {
    public static void main(String[] args) {
        //通过匿名内部类实现多线程
        /*Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {
                    System.out.println(i + "~~~~~~~~~~~~~~~~~~~~~~~~");
                }
            }
        };*/
        //通过Lambda表达式实现多线程
        Runnable runnable = () -> {
            for (int i = 0; i < 50; i++) {
                System.out.println(i + "~~~~~~~~~~~~~~~~~~~~~~~~");
            }
        };
        Thread thread1 = new Thread(runnable);
        thread1.start();
        //主线程
        for (int j = 0; j < 50; j++) {
            System.out.println(j + "===============================");
        }
    }
}

在这里插入图片描述

通过实现Runnable接口创建并启动多线程步骤

相关代码如图所示
请添加图片描述

实现Runnable后通过匿名内部类或Lambda表达式来启动多线程

package at.guigu.day10;

public class TestMyThreadTwo {
    public static void main(String[] args) {
        //通过匿名内部类实现多线程
        /*Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {
                    System.out.println(i + "~~~~~~~~~~~~~~~~~~~~~~~~");
                }
            }
        };*/
        //通过Lambda表达式实现多线程
        Runnable runnable = () -> {
            for (int i = 0; i < 50; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + i + "~~~~~~~~~~~~~~~~~~~~~~~~");
            }
        };
        //创建子线程1
        Thread thread1 = new Thread(runnable);
        thread1.start();//启动子线程1
        //创建子线程2
        Thread thread2 = new Thread(runnable);
        thread2.start();//启动子线程2
        //创建并启动子线程3
        new Thread(runnable).start();
        //主线程
        for (int j = 0; j < 50; j++) {
            System.out.println(Thread.currentThread().getName() + ":" + j + "===============================");
        }
    }
}

在这里插入图片描述

Thread类

构造方法(构造器)

构造器类型解释
public Thread()分配一个新的线程对象
public Thread(String name)分配一个指定名字的新的线程对象
public Thread(Runnable target)分配一个带有指定目标的新的线程对象。Runnable target为接口的实现类
public Thread(Runnable target,String name)分配一个带有指定目标和指定名字的新的线程对象

Thread类中的常用方法

常用方法一

方法解释
public void run()此线程要执行的任务,在此处定义代码
public String getName()获取当前线程的名称
public void setName(String name)指定当前线程的名称
public final boolean isAlive()测试线程是否处于活动状态,即当前线程是否正在运行。(注意:只有处于激活状态的线程才可以去抢CPU)(线程生命周期:随着线程的开始而开始,结束而结束。所以当线程开始时为True,结束时为False)
public static Thread currentThread()返回对当前正在执行的线程对象的引用(即返回当前代码所运行的这个线程的对象。举例说明:假设现在该方法是在main方法中调用的吗,因为main方法是主线程中的方法,所以该方法返回的是当前正在执行的线程对象的引用即主线程)
public final int getPriority()返回线程的优先级
public final void setPriority(int newPriority)改变线程的优先级,newPriority范围在[0,10],通常推荐设置Thread类的三个优先级常量(直接由类名调用):MAX_PRIORITY(10):最高优先级 MIN_PRIORITY(1):最低优先级 NORM_PRIORITY(5):普通优先级,默认情况下main线程具有普通优先级

常用方法二

方法解释
public void start()开启子线程
public static void yield()让当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其它线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了该方法暂停后,线程调度又将其调度出来重新执行(说白了就是线程放掉CPU后重新回到了抢CPU的队列,在控制台看不出来线程放掉过CPU)
public static void sleep(long millis)(参数为long型的整数)使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行,即线程会暂时休眠mmillis毫秒,在休眠状态下不会去抢CPU,且在此期间当前线程会释放 CPU 资源,允许其他线程在此期间运行)注意:休眠状态下不释放锁对象,这一点与Object根父类中的wait()方法刚好相反
void join()让当前线程(正在执行的线程)等待调用该方法的线程执行完毕后再进行执行
void join(long millis)让当前线程等待调用该方法的线程终止的时间最长为millis毫秒,如果millis时间到,则当前线程会结束等待并继续执行(注意:当前线程会暂时将cpu让给调用该方法的线程若干时间使其执行,等时间到后会重新夺回cpu控制权并进行执行)
void join(long millis, int nanos)等待该线程终止的时间最长为millis毫秒+nanos纳秒
线程结束解释
自然死亡一个线程的run方法执行完,线程会自然停止
意外死亡线程遇到未捕获处理的异常,则会挂掉
public final void stop()强迫线程停止执行(该方法不安全,且已经被标记过时,不建议使用)

注意:sleep方法当休眠时间结束后,线程会重新进入可运行状态(Runnable State),等待操作系统的调度分配CPU时间片,不会立即获得CPU执行权。且休眠的时间是相对时间,线程休眠后不会准确地在指定的毫秒数后醒来,而是要等待操作系统的调度,来与其它线程一起竞争CPU执行权,因此哪个线程获得CPU执行权取决于操作系统的调度算法和优先级等因素。

Thread构造器及方法使用示例

继承Thread类

  • 创建Thread的子类------使用有参构造器
    请添加图片描述
  • 创建Thread类子类的实例,创建多线程并命名
    请添加图片描述
  • 运行结果
    请添加图片描述

实现Runnable接口

  • 线程类实现接口并实现run()方法
    请添加图片描述
  • 创建多个子线程并命名且在控制台输出时输出线程名
    请添加图片描述
  • 运行结果如图所示
    请添加图片描述

获取或设置线程优先级

  • 每个线程都有一定的优先级,优先级高的线程将获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。
    请添加图片描述
  • 通过优先级常量来设置线程优先级
    请添加图片描述
    请添加图片描述
  • 注意事项
    (1)实际项目中控制线程用的手段比现在更复杂
    (2)优先级低的线程不一定抢不过优先级高的线程,即并不是说谁的优先级高就一定先执行谁.

sleep()方法

  • sleep()方法需要用到异常捕获try…catch块的原因

在Java中,Thread类的sleep()方法会抛出InterruptedException异常,这是因为当线程处于sleep状态时,如果另一个线程中断了它,那么该线程会抛出InterruptedException异常。因此,在使用sleep()方法时,需要使用try...catch语句来捕获并处理InterruptedException异常,以确保程序的正常运行。

请添加图片描述

join()方法

  • join()方法需要用到异常捕获try…catch块的原因

在Java中,Thread类的join()方法可以让当前线程等待另一个线程执行完毕后再继续执行。如果在等待过程中,另一个线程被中断或者发生异常,那么join()方法会抛出InterruptedException异常。因此,在使用join()方法时,需要使用try...catch语句来捕获并处理InterruptedException异常,以确保程序的正常运行。

package com.atguigu.thread;

public class TestJoin01 {
    public static void main(String[] args) {
        MyThreadThree myThreadThree = new MyThreadThree();
        MyThreadFour myThreadFour = new MyThreadFour(myThreadThree);
        myThreadThree.start();
        //设置MyThreadFour的优先级,目的是为了让测试更清晰
        myThreadFour.setPriority(Thread.MAX_PRIORITY);
        myThreadFour.start();
    }
}

class MyThreadThree extends Thread{
    @Override
    public void run() {
        for (int i = 100; i <=120 ; i++) {
            System.out.println(this.getName() + ":" + i + "~~~~~~~~~~~~~~MyThreadThree~~~~~~~~~~~~");
        }
    }
}

class MyThreadFour extends Thread {
    private MyThreadThree myThreadThree;
    //若只写了一个显式有参构造器没有写无参的构造器,所以就会默认没有无参构造器,除非将其写出来
    public MyThreadFour(MyThreadThree myThreadThree){
        this.myThreadThree = myThreadThree;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(this.getName() + ":"+i+"---------------MyThreadFour----------------");
            //当MyThreadFour执行到一半时,让MyThreadThree线程插队。即当前正在执行的线程暂停执行,当当前插队的进程执行完毕后再次开始执行
            if(i == 50){
                try {
                    myThreadThree.join(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

请添加图片描述

守护线程

  • 定义
    守护线程随着被守护线程的开始而开始,随着被守护线程的结束而结束。你在哪个线程中创建的守护线程对象,那它就守护哪个线程

在 Java 中,可以通过 setDaemon(boolean on)方法将一个线程设置为守护线程。当一个线程被设置为守护线程后,它会随着程序的结束而自动结束,不会阻止程序的终止。如果一个线程没有被设置为守护线程,则它是用户线程,程序会等待所有的用户线程结束后才会终止。

setDaemon(boolean on) 方法的参数 ontrue 时表示将线程设置为守护线程,为 false时表示将线程设置为用户线程。需要注意的是,setDaemon(boolean on) 方法必须在 start()方法之前调用,否则会抛出一个 IllegalThreadStateException 异常。

在调用 setDaemon(boolean on) 方法之前,必须先创建一个新的线程对象,并将其传递给该方法,否则会抛出一个NullPointerException 异常。

  • 作用
    主要用于执行一些辅助任务,比如:垃圾回收、内存管理等。通过Thread类的setDaemon(boolean on)方法来设置线程是否为守护线程
  • 示例
    (方式1)利用接口实现守护线程
package com.atguigu.thread;
//利用接口实现守护线程
public class TestProtectLine {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new MyRunnable());
        t.setDaemon(true);  // 设置为守护线程
        t.start();

        // 主线程休眠5秒后退出
        Thread.sleep(5000);
        System.out.println("Main thread is done!");
    }
    
//守护线程主代码
//此为静态成员内部类
    static class MyRunnable implements Runnable {
        public void run() {
            while (true) {
                System.out.println("Daemon thread is running...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}


(方式2)利用继承Thread类实现守护线程

package com.atguigu.thread;

//通过继承Thread类来实现守护线程
public class TestProtectLine2 {
    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.setDaemon(true);  // 设置为守护线程
        t.start();

        // 主线程休眠5秒后退出
        Thread.sleep(5000);
        System.out.println("Main thread is done!");
    }
//守护线程代码
    static class MyThread extends Thread {
        public void run() {
            while (true) {
                System.out.println("Daemon thread is running...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}


龟兔赛跑例题

题目:跑道长30米,乌龟1m每秒。兔子10m每秒,乌龟每跑完10m休眠时间1s,兔子每跑完10m休眠时间10s

  • 乌龟类
package com.atguigu.day10;

public class Tortoise extends Thread{
    @Override
    public void run(){
        for (int i = 0; i < 30; i++) {
            //循环一次休眠一秒就代表每秒跑一米
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("乌龟跑了" + i + "米");
            if (i % 10 == 0 && i > 0) {
                //乌龟每跑10米休眠1s
                try {
                    Tortoise.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        System.out.println("乌龟到达终点");
    }
}

  • 兔子类
package com.atguigu.day10;

public class Rabbit extends Thread{
    @Override
    public void run(){
        for (int i = 0; i < 30; i++) {
            //兔子10m每秒。那就让1m休眠0.1s
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("兔子跑了" + i + "米");
            if (i%10 == 0 && i > 0) {
                try {
                    Rabbit.sleep(10000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        System.out.println("兔子到达终点");
    }
}

  • 实体类
package com.atguigu.day10;

public class RabbitTortoioseRace {
    public static void main(String[] args) {
        Rabbit rabbit = new Rabbit();
        Tortoise tortoise = new Tortoise();
        rabbit.start();
        tortoise.start();
    }
}

在这里插入图片描述

线程安全问题

  • 定义
    多个线程访问(对其进行读和写操作)统一资源(如:同一个变量、文件、记录等)而引起的问题
  • 引起线程安全问题的原因
    当一个线程(记为A)对一个资源的操作还未结束时,另一个线程(记为B)已经进来对其进行操作了,B对其操作结束后,A开始对其进行操作时发现数据已经被改了,从而引起了错误

以卖票问题为例

  • 实体类
package com.atguigu.threadsafe;
//买票问题bug代码
public class SellTicketOne extends Thread{
    /*
    普通属性随着对象的创建会在堆中被创建,所以每new一个对象就会有一个tickets
    如果这样的话,最后就不是卖100张票了,比如你本来想用三个线程卖100张票
    但此时由于tickets为普通属性,所以相当于你的三个线程各卖100张票。所以
    不能用private int tickets = 100;
    所以需要用静态属性,因为静态属性会随着类的加载在方法区中存在,且共用在方法区中的静态属性
     */
    private static int tickets = 100;
    @Override
    public void run() {
        while (true) {
            /*此处不写==原因:售票系统肯定是多线程,当不同的用户买票时,票数都会-1,
            所以可能在一个线程中票数还是大于0,但是在另一个线程中票数是小于0的,只要
            有一个线程中的票数小于等于0,就说明没票了,就可以跳出循环,所以应该用<=更安全
             */
            if (tickets <= 0) {
                System.out.println("票以售完");
                break;
            }
            /*线程休眠0.5s来模拟由于代码过多导致执行时间延长的现象
            但加上休眠的代码后会出现问题:假设第一个线程在买编号为a的这张票,由于长时间停留在付款页面(此处用
            线程休眠来代替),导致另一个线程已经将编号为a的票卖出去了,所以运行后会出现卖出同一张票的结果,由此出现了
            线程安全问题
             */
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.getName() + "线程卖出一张票,当前剩余" + (--tickets) + "张票");
        }
    }
}

  • 测试类
    请添加图片描述
  • 产生线程安全问题的两要素
    (1)要有多个线程
    (2)多个线程访问同一个资源

线程安全问题解决方法(同步机制原理和同步方法)

方法一:同步机制原理

  • 定义
    同步机制是指多个线程在访问共享资源时,通过协调各自的执行顺序,以避免出现竞态条件(race condition)和数据不一致的问题。Java中的同步机制主要是通过对象锁(也称为监视器锁,此为最基本的同步锁)来实现的。
  • 同步锁
    相当于给某段代码加“锁”,任何线程想要执行这段代码,都要先获得“锁”
  • 实现同步锁的两种方式
    (1)同步代码块
    (2)同步方法
实现同步锁的方式一:同步代码块–常用字符串做锁对象
  • 语法格式
    synchronized关键字可以用于某个区块前面,表示只对这个区块的资源实行互斥访问请添加图片描述
    同步锁为锁对象(即用对象来当锁),不同的线程去抢锁对象,只要某个线程(记为A线程)抢到锁对象后,其它线程就无法执行需要同步操作的代码,此时其它线程只能等待。
常见的锁对象
  • String(字符串)做锁对象------可靠,均能锁得住
    (1)通过继承Thread类
    在这里插入图片描述
    在这里插入图片描述
    (2)通过实现接口
    在这里插入图片描述
    在这里插入图片描述
  • 类名.class做锁对象------为当前类的类类对象------可靠,均能锁得住
    (1)通过继承Thread类
    在这里插入图片描述
    在这里插入图片描述
    (2)通过实现接口
    在这里插入图片描述
    在这里插入图片描述
  • 类属性做锁对象—有风险
    注意:
    (1)可能锁得住也可能锁不住
    (2)在测试中用所有类的根父类Object做锁对象,对于“通过继承Thread类”和“通过实现接口”目前时均锁住
  • 由于任何一个类的对象都可以做锁对象,所以可用当前类的对象(this)做锁对象—有风险

(1)对于继承Thread类的方式不可行

原因:在继承Thread类中,多线程肯定要创建多个当前实体类的对象,而此时你用实体类的对象做了锁对象,多个实体类的对象在最初开始都拥有该锁对象,所以锁不住

在这里插入图片描述
在这里插入图片描述
(2)对于实现接口的方式可行

原因:
因为在实现接口的方式下,是通过创建Thread类的对象来实现多线程的,而锁对象是实体类的对象,所以在最初开始,多线程都并未拥有锁对象,所以可以锁得住

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

  • 注意事项
    (1)当一个线程抢到锁对象后,在执行代码的过程中,若由于CPU使用权限到期退出后,当其在拿到CPU控制权后,其会从刚才停止执行的那个位置开始继续执行,而不是重头开始执行
    (2)拿不到锁对象的线程即使抢到CPU也没用,因为只要某个线程抢到锁对象,则必须执行完里面的代码后才会释放锁,其它线程只有在抢到锁并且获得CPU(缺一不可)的情况下才可以执行,反之则不可
    (3)任何一个类的对象都可以做锁对象,所以锁对象是哪个类的对象不关心,一般是所有类的根父类Object对象,且为静态的根父类Object对象

  • 买票问题出错最终解决方案
    (1)通过继承Thread类
    在这里插入图片描述
    在这里插入图片描述
    (2)通过实现接口
    在这里插入图片描述
    在这里插入图片描述

方法二:同步方法

  • 什么时候用
    当方法的方法体内的所有方法均在同一个同步代码块中时,此时可以不用同步代码块,改用同步方法
  • 语法格式
    在这里插入图片描述
  • 锁对象
    (1)若同步方法是静态的同步方法,则锁对象是当前类的类类对象
    (2)若同步方法是普通的同步方法(即非静态同步方法),则锁对象是this
  • 特点
    (1)安全性高,效率性低
    (2)同步方法的锁对象看不到,而同步代码块的锁对象能看到
  • 代码示例
    (1)通过继承Thread类
package com.atguigu.threadsafe;

public class SellTicketFive {
    public static void main(String[] args) {
        TicketSaleThread t1 = new TicketSaleThread();
        TicketSaleThread t2 = new TicketSaleThread();
        TicketSaleThread t3 = new TicketSaleThread();

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

class TicketSaleThread extends Thread{
    private static int total = 100;
    public void run(){//直接锁这里,肯定不行,会导致,只有一个窗口卖票
        while(total>0) {
            saleOneTicket();
        }
    }

    public synchronized static void saleOneTicket(){//锁对象是TicketSaleThread类的Class对象,而一个类的Class对象在内存中肯定只有一个
        if(total > 0) {//不加条件,相当于条件判断没有进入锁管控,线程安全问题就没有解决
            System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + --total);
        }
    }
}

在这里插入图片描述
(2)通过实现Runnable接口
注意:只要敢用普通属性(即不是静态属性),以及this作为锁对象,说明只能创建一个对象

package com.atguigu.threadsafe;

public class SellTicketSix {
    public static void main(String[] args) {
        TicketSaleRunnable tr = new TicketSaleRunnable();
        Thread t1 = new Thread(tr, "窗口一");
        Thread t2 = new Thread(tr, "窗口二");
        Thread t3 = new Thread(tr, "窗口三");

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

class TicketSaleRunnable implements Runnable {
    private int total = 100;

    public void run() {//直接锁这里,肯定不行,会导致,只有一个窗口卖票
        while (total > 0) {
            saleOneTicket();
        }
    }
    public synchronized void saleOneTicket(){//锁对象是this,这里就是TicketSaleRunnable对象,因为上面3个线程使用同一个TicketSaleRunnable对象,所以可以
        if(total > 0) {//不加条件,相当于条件判断没有进入锁管控,线程安全问题就没有解决
            System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + --total);
        }
    }
}

在这里插入图片描述

等待唤醒机制

线程间通信

  • 定义
    线程间通信是指多个线程之间通过共享内存或消息传递等方式来实现协作的过程。在多线程编程中,由于多个线程之间共享同一份数据,因此需要通过线程间通信来确保数据的正确性和一致性。
  • 目的
    实现线程之间的协作,以完成特定的任务或实现特定的功能。例如,当一个线程需要等待另一个线程完成某个操作后才能继续执行时,就需要通过线程间通信来实现等待和唤醒的过程。
  • 什么时候能够用到线程间通信
    多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。比如:线程A用来生成包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,此时B线程必须等到A线程完成后才能执行,那么线程A与线程B之间就需要线程通信,即—— 等待唤醒机制。

等待唤醒机制

  • 定义
    等待唤醒机制是多线程编程中用于线程间协作的一种机制。它允许一个线程等待特定条件的发生,而另一个线程在满足条件时通知等待的线程继续执行。
  • 等待唤醒机制通常由以下几个关键元素组成
    (1)锁(Lock)或监视器(Monitor):用于确保在多线程环境下对共享资源的访问是同步的。在Java中,可以使用关键字synchronized或显式的Lock接口来实现。
    (2)等待(Wait):一个线程在获得锁的情况下,通过调用锁对象的wait()方法来释放锁并进入等待状态。等待状态的线程将暂停执行,直到其他线程通过唤醒操作通知它继续执行。
    (3)唤醒(Notify):一个线程在获得锁的情况下,通过调用锁对象的notify()或notifyAll()方法来唤醒一个或所有正在等待的线程。被唤醒的线程将从等待状态转换为可运行状态,等待获取锁后继续执行。
  • 注意
    (1)等待唤醒机制只能在已经获取锁的情况下使用,因为只有持有锁的线程才能释放锁并进入等待状态。另外,等待唤醒机制不保证线程的执行顺序,被唤醒的线程可能不是按照先后顺序执行,因此在使用时需要谨慎考虑。
    (2)wait()notify()notifyAll()是由锁对象调用的,由于锁对象是任何一个类的对象都可以担任,任何一个类都要能够调用wait()notify()notifyAll(),所以wait()notify()notifyAll()是属于根父类Object中的普通方法。可详见day06中Object跟父类中的若干个方法
    (3)被唤醒的线程进入可运行状态时,就已经获得了CPU的执行权,只需等待操作系统为其分配CPU时间片。操作系统的线程调度器负责根据调度算法从可运行状态的线程中选择一个来运行。
  • sleep()与wait()方法的区别
    (1)sleep()方法释放CPU但不释放锁,wait()方法释放CPU的同时也释放锁
    (2)sleep()方法指定休眠的时间,wait()方法可以指定时间也可以无限等待直到notify或notifyAll
    (3)sleep()方法在Thread类中声明的静态方法,wait()方法是在Object类中声明的普通方法

一个生产者与一个消费者的问题

以代码为例

  • 生产者
package com.atguigu.test1;
//一个生产者
public class Producer extends Thread{
    private Stone stone;//Producer需要调用Stone,所以做一个Stone的属性
    //构造器
    public Producer(Stone stone) {
        this.stone = stone;
    }
    @Override
    public void run() {
        while (true) {
            stone.add();
        }
    }
}

  • 消费者
package com.atguigu.test1;
//一个消费者
public class Customer extends Thread{
    private Stone stone;
    public Customer(Stone stone) {
        this.stone = stone;
    }
    @Override
    public void run() {
        while (true) {
            stone.get();
        }
    }
}

  • 仓库
package com.atguigu.test1;
//仓库
/*
注意:只要敢用普通属性,以及this作为锁对象,说明只能创建一个对象
 */
public class Stone {
    private int items = 20;//仓库库存上限

    //生产商品
/*    public void add() {
        synchronized (this) {

        }
    }*/
    //等同于
    public synchronized void add() {
        if (items >= 20) {
            System.out.println("库存已达上限,暂停生产,先消费===============");
        }
        try {
            this.wait();//当前线程进入等待状态
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(Thread.currentThread().getName()
                + "生产线程生产了一个商品,当前库存量为:"
                + (++items)+ "~~~~~~~~~~~");
        this.notify();//随机唤醒一个正在等待的线程
    }
    //消费商品
/*    public void get() {
        synchronized (this) {

        }
    }*/
    //等同于
    public synchronized void get() {
        if (items <= 0) {
            System.out.println("商品库存已为0,暂停消费先生产=================");
            try {
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println(Thread.currentThread().getName()
                + "消费线程消费了一个商品,当前库存数量为:"
                + (--items) + "---------------");
        this.notify();//随机唤醒一个正在等待的线程
    }
}

  • 测试类
package com.atguigu.test1;

public class TestCusProSto {
    public static void main(String[] args) {
        Stone stone = new Stone();
        Producer producer = new Producer(stone);
        Customer customer = new Customer(stone);
        producer.start();
        customer.start();
    }
}

在这里插入图片描述

多个生产者与多个消费者的问题

详见代码示例

  • 生产者
package com.atguigu.test2;
//多个生产者
public class Producer extends Thread{
    private Stone stone;//Producer需要调用Stone,所以做一个Stone的属性
    //构造器
    public Producer(Stone stone) {
        this.stone = stone;
    }
    @Override
    public void run() {
        while (true) {
            stone.add();
        }
    }
}

  • 消费者
package com.atguigu.test2;
//多个消费者
public class Customer extends Thread{
    private Stone stone;
    //构造器
    public Customer(Stone stone) {
        this.stone = stone;
    }
    @Override
    public void run() {
        while (true) {
            stone.get();
        }
    }
}

  • 仓库
package com.atguigu.test2;
//多个生产者多个消费者一个仓库的问题
public class Stone {
    private int items = 20;//仓库库存上限
    //生产商品
/*    public void add() {
        synchronized (this) {

        }
    }*/
    //等同于
    public synchronized void add() {
        /*
        if改为while的原因:
        假设刚开始时items = 20达到了库存上限,子线程producer1此时进入获得CPU及锁对象开始执行。当他发现
        items>=20时(即库存达到上限)立即执行this.wait()方法使自身进入等待状态并释放锁对象和CPU;此时子线程
        producer2获得CPU以及锁对象开始执行add()方法,由于此时items = 20达到上限,所以也会执行wait()方法释放
        CPU以及锁对象;此时消费者customer1获得了锁对象和CPU,执行了一次消费使得items = 19后唤醒了正在等待的线
        程producer1和producer2,假设此时customer1释放了CPU以及对象,且恰好被producer获取了锁对象和CPU,则会
        在最开始暂停执行的那个地方也就是this.wait()处重新开始继续向下执行,然后在生产一个商品后item又达到了20,此
        时它进行了一次唤醒线程,并假设此时释放了CPU和锁对象,此时假设producer2被唤醒了并获得了锁对象,那么其就会从最
        开始进入等待状态的那行代码处开始向下执行(即this.wait()处开始向下执行),此时当执行到try...catch块内的代码
        后会返回判断目前的items是否是>=20,然后才会继续往下执行,假设不是while循环,则不会再返回进行一个items>=20的
        判断,而是会继续向下执行,此时就会导致超过库存最大上限,所以要在多个消费者和多个生产者问题中要将if改为while
         */
        while (items >= 20) {
            System.out.println("库存已达上限,暂停生产,先消费===============");
            try {
                this.wait();//当前线程进入等待状态并释放锁对象
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println(Thread.currentThread().getName()
                + "生产线程生产了一个商品,当前库存量为:"
                + (++items)+ "~~~~~~~~~~~");
        this.notifyAll();//唤醒所有正在等待的线程
    }
    //消费商品
/*    public void get() {
        synchronized (this) {
        }
    }*/
    //等同于
    public synchronized void get() {
        while (items <= 0) {
            System.out.println("商品库存已为0,暂停消费先生产=================");
            try {
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println(Thread.currentThread().getName()
                + "消费线程消费了一个商品,当前库存数量为:"
                + (--items) + "---------------");
        this.notifyAll();//唤醒所有正在等待的线程
    }
}

  • 测试类
package com.atguigu.test2;
//多个生产者和多个消费者
public class TestCusProSto {
    public static void main(String[] args) {
        Stone stone = new Stone();
        Producer producer1 = new Producer(stone);
        Producer producer2 = new Producer(stone);
        Producer producer3 = new Producer(stone);
        Customer customer1 = new Customer(stone);
        Customer customer2 = new Customer(stone);
        Customer customer3 = new Customer(stone);
        producer1.start();
        producer2.start();
        producer3.start();
        customer1.start();
        customer2.start();
        customer3.start();
    }
}

在这里插入图片描述

线程生命周期------需要知道两个状态之间转换的方法

JDK1.5之前

在这里插入图片描述

  • 新建
    当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
  • 就绪
    当线程对象调用了start()方法之后,就不一样了,线程就从新建状态转为就绪状态

注意:
(1)程序只能对新建状态的线程调用start(),并且只能调用一次,如果对非新建状态的线程,如已启动的线程或已死亡的线程调用start()都会报错IllegalThreadStateException异常。
(2)就绪状态可以抢CPU

  • 运行
    如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程体代码,则该线程处于运行状态。如果计算机只有一个CPU,在任何时刻只有一个线程处于运行状态,如果计算机有多个处理器,将会有多个线程并行(Parallel)执行。

运行状态怎么进入到死亡状态:
(1)run()方法运行结束
(2)运行过程中出现错误(Error)和异常(Wxception)

  • 阻塞------进入阻塞状态的线程不会去抢CPU
    当在运行过程中的线程遇到如下情况时,线程会进入阻塞状态:

(1)线程调用了sleep()方法,主动放弃所占用的CPU资源;
(2)线程试图获取一个同步监视器,但该同步监视器正被其他线程持有;
(3)线程执行过程中,同步监视器调用了wait(),让它等待某个通知(notify);
(4)线程执行过程中,同步监视器调用了wait(time)
(5)线程执行过程中,遇到了其他线程对象的加塞(join);
(6)线程被调用suspend方法被挂起(已过时,因为容易发生死锁);

当前正在执行的线程被阻塞后,其他线程就有机会执行了。针对如上情况,当发生如下情况时会解除阻塞,让该线程重新进入就绪状态,等待线程调度器再次调度它:

(1)线程的sleep()时间到;
(2)线程成功获得了同步监视器;
(3)线程等到了通知(notify);
(4)线程wait的时间到了
(5)加塞的线程结束了;
(6)被挂起的线程又被调用了resume恢复方法(已过时,因为容易发生死锁);

  • 死亡
    线程会以以下三种方式之一结束,结束后的线程就处于死亡状态:

(1)run()方法执行完成,线程正常结束
(2)线程执行过程中抛出了一个未捕获的异常(Exception)或错误(Error)
(3)直接调用该线程的stop()来结束该线程(已过时,因为容易发生死锁)

JDK1.5之后

说白了就是将阻塞状态分为了两种状态—即定时阻塞(如:sleep(1000))和不定时阻塞(如:wait()和join())

释放锁操作与死锁

  • 死锁定义
    是多线程编程中的一种特定情况,指两个或多个线程被永久地阻塞,无法继续执行,并且彼此互相等待对方释放资源的状态。在死锁中,每个线程都在等待其他线程释放资源,导致所有线程都无法继续执行下去,形成了一种僵局。
  • 代码示例
package com.atguigu.test3;
//死锁
public class TestDeadLock {
    public static void main(String[] args) {
        LockSmith lockSmit = new LockSmith();
        Owner owner = new Owner();
        DeadLockThread dt1 = new DeadLockThread(lockSmit, owner, true);//锁匠线程
        DeadLockThread dt2 = new DeadLockThread(lockSmit, owner, false);//业主线程
        dt1.start();
        dt2.start();
    }
}

class LockSmith{
    public void say() {
        System.out.println("给我看房本,我给你开门");
    }
    public void work() {
        System.out.println("给你开门");
    }
}

class Owner extends Thread {
    public void say() {
        System.out.println("给我开门,我给你看房本");
    }
    public void work() {
        System.out.println("给你看房本");
    }
}

class DeadLockThread extends Thread {
    private LockSmith lockSmit;
    private Owner owner;
    private boolean flag;
    //构造器
    public DeadLockThread (LockSmith lockSmit, Owner owner, boolean flag) {
        this.lockSmit = lockSmit;
        this.owner = owner;
        this.flag = flag;
    }
    @Override
    public void run() {
        if (flag) {
            //锁匠线程
            synchronized (lockSmit) {
                lockSmit.say();
                synchronized (owner) {
                    lockSmit.work();
                }
            }
        }else {
            //业主线程
            synchronized (owner) {
                owner.say();
                synchronized (lockSmit) {
                    owner.work();
                }
            }
        }
    }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT机器猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值