JAVA多线程

多线程概念

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

简单理解为:应用软件中互相独立,可以同时运行的功能

例如,联想的电脑管家,我们运行起来它就是个进程

image-20240529101621953

而它左边导航栏中的这几个功能就可以看做六个线程

image-20240529101707791

所以综上所述,多线程的意思就是运行的功能比较多,就形成了多线程

进程:进程是程序的基本执行实体

那么我们可以理解为,只要我们打开一个程序,程序执行后就是一个进程。例如

image-20240529101353000

为什么要使用多线程

那么为什么我们要用多线程呢?

我们可以先来看下面这个例子

image-20240529102427340

小白在工厂打螺丝,只负责一条线路,但是这条线路上只会每隔十分钟发一个货物下来,那么小白就可以办完一次货物,休息个九分多钟,黑老板看到了当然不愿意了,所以由给它安排了几条线路
image-20240529102621387

这下小白就不能偷懒了,本来原本可以休息十分钟,现在可能只能休息三分钟

同样我们程序也是如此,以往我们写的代码都是单线程的程序

image-20240529102735057

从第一条编译到最后一条,每条代码都要等着前一条代码执行完才能够执行下一条,浪费了很多时间,所以要基于多线程去操作程序

多线程的应用场景

image-20240529103203343

例如,我们打游戏的时候进入游戏界面,就是在等待加载一些资源文件,这里就不得不提一个二字游戏

image-20240529103625512

这里就是在等待游戏多线程加载

并发和并行

并发:在同一时刻,有多个指令在单个CPU上交替执行

并行:在同一时刻,有多个指令在多个CPU上同时执行

这样说有点抽象,那么我们来看一张图

image-20240529103959842

上面图就能反应并发的效果,我们在玩电脑时,一会儿要弄鼠标,一会儿要拿可乐,一会儿要来根小烟,那么我们的手就当成是CPU,其他的当成线程,这就是并发

同样我们可以看下面这张图

image-20240529104428732

看到这里可能有些人会有疑惑,我电脑只有一个CPU啊,确实我们电脑都是只有一个CPU,但是我们CPU标注多少核多少线程的

image-20240529104609481

这些线程的数量就是我们能够同时运行多少个线程,当我们运行很多线程的时候,线程就会在在这些线程中随机运行,来回切换

image-20240529104738807

简单来说可以理解为,单车道并发,多车道并行

多线程的实现方式

在多线程中一共有三种方式实现多线程

  1. 继承Thread类的方式进行实现
  2. 实现Runnable接口的方式进行实现
  3. 实现Callable接口和Future接口方式实现

Thread实现

那么我们来进行第一种方法继承Thread,首先我们创建一个类继承Thread

image-20240529153855614

然后我们来重写run方法

image-20240529154112378

那么我们在这里打印100次helloword吧

image-20240529154201472

这里的getName方法是获取线程的名称,那么我们来创建我们自己写的线程利用setName设置名字,然后运行

image-20240529154408786

然后我们看运行结果就可以看到线程1和2交替运行

image-20240529154458943

完整代码:

//EXthread
public class EXThread extends Thread{

    @Override
    public void run() {
        //书写要线程干什么事情
        for (int i = 0; i < 100; i++) {
            System.out.println(getName()+"helloword");
        }
    }
}

//main
    public static void main(String[] args) {
        EXThread t1 = new EXThread();
        t1.setName("线程1");
        EXThread t2 = new EXThread();
        t2.setName("线程2");

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

    }

Runnable接口实现

接着我们来实现第二种方式,实现Runnable接口,首先我们创建一个类然后实现Runnable接口,我们同样写上打印一百个Helloword

image-20240529160146092

解释一下上面代码,为啥我们需要利用Thread.currentThread来获取当前线程的对象

首先第一种直接继承Thread是让类直接与Thread产生父子关系,子类当然可以直接调用父类的方法,而在实现Runnable接口的时候,我们其实在定义一个“任务”,而不是定义一个具体的线程。

这个“任务”可以被多个线程执行。因此,当我们把这个“任务”传递给一个Thread对象并启动线程来执行时,我们需要通过Thread.currentThread().getName()来获取当前执行任务的线程名称。

然后我们来创建主类

image-20240529161512332

正如上面所说,我们需要把我们定义的任务传递给线程,让线程来执行,然后我们看运行效果

image-20240529161629026

完整代码:

//IMRunnable
public class IMRunnable implements Runnable{
    @Override
    public void run() {
        Thread t = Thread.currentThread();
        //这里面写需要进行多线程的方法
        for (int i = 0; i < 100; i++) {
            System.out.println(t.getName() +"helloword");
        }
    }
}

//MyRunnable
public class MyRunnable {
    public static void main(String[] args) {
        //创建IMRunnable对象
        //表示多线程要执行的任务
        IMRunnable i1 = new IMRunnable();

        //创建线程对象
        Thread t1 = new Thread(i1);
        t1.setName("线程1");
        Thread t2 = new Thread(i1);
        t2.setName("线程2");

        //启动
        t1.start();
        t2.start();
    }
}

Callable和Future接口实现

看了前面俩个多线程的实现,我们可以发现一个共同点,那就是如果我们需要获取多线程结果怎么办 ,那我们就可以考虑实现Callable接口

image-20240529164056417

可以看到我们的Callable是有返回值的,接着我们来实现主类

image-20240529164404943

挨个解释一下,Callable也是和Runnable一样是创建一个任务,但是Callable因为有返回对象是传递进Future中进行管理,然后传入线程对象

image-20240529165132974

完整代码

//MyCallable
public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= 100; i++) {
            sum+=i;
        }
        return sum;
    }
}
//ThreadDemo
public class ThreadDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //表示要执行的任务
        MyCallable mc = new MyCallable();
        //管理多线程的运行结果
        FutureTask<Integer> ft = new FutureTask<>(mc);
        //创建多线程对象
        Thread t1 = new Thread(ft);
        //启动
        t1.start();

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

Thread和Runnable区别

可以再深入讲讲Thread和Runnable的区别的,

因为使用MyThread会创建多个实例,而Thread(MyRunnable)创建的多线程可’共用’一个MyRunnable实例。

所以,在MyThread中,synchronized锁是必须static的,且若想把while中的代码块直接抽取成synchronized修饰方法也会导致锁失效,因为同步方法默认使用的锁是this(this指向MyThread的多个实例),除非同时再加上static;

而在接口实现的Thread(MyRunnable)中反而可以不使用static修饰synchronized锁,或者while中的代码块直接抽取成非static的synchronized修饰方法也没问题(this指向的MyRunnable唯一实例)。

记得在哪里也看到过不建议直接继承重写成MyThread类。我认为用Thread(Runnable)实现好在,能把MyRunnable通过面向对象的思想去调用,比如new一个MyRunnable实现,即创建了一个新的xx电影的票池和一个新的xx电影的锁。

常见的成员方法

三种线程都实现后,我们来看看有哪些常用的方法

image-20240529165537076

前置准备:为了方便演示,我们来创建一个线程对象,还是打印100个helloworld

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

1.getname()

作用:获取线程名字

这个方法我们在前面已经用过了,就是获取线程的名字,那么我们如不获取名字,直接getname会发生什么呢

    public static void main(String[] args) {
        //创建线程对象
        Thread t1 = new MyThread();
        Thread t2 = new MyThread();

        //开启线程
        t1.start();
        t2.start();
    }

image-20240529170602374

可以看到如果我们不设置线程名字会默认Thread-XX这种形式出现

那么我们来看一下源码是怎么回事,按Ctrl+N 搜索Thread

image-20240529170836663

然后我们ctrl+F12找到它的空参构造

image-20240529170915212

然后我们就可以看到

image-20240529170943146

然后我们可以看看nextThreadNum是干什么的

image-20240529171024340

从这里看出这就是个序号,默认自增开始值为0

2.setname()

作用:设置线程名字

setname()方法呢,我们上面已经使用过了,就是用来设置线程的名字所以我这里就直接贴代码和运行结果

public class ThreadDemo {
    public static void main(String[] args) {
        //创建线程对象
        Thread t1 = new MyThread();
        Thread t2 = new MyThread();
        t1.setName("线程1");


        //开启线程
        t1.start();
        t2.start();
    }
}

image-20240529171327948

可以看到没有设置名字的默认还是Thread-XX的形式,同样的我们可以在创建线程的时候传递名字,只需要我们重写构造方法即可

//MyThread
public class MyThread extends Thread{
    public MyThread() {
        super();
    }

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

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

//Mythread
    public static void main(String[] args) {
        //创建线程对象
        Thread t1 = new MyThread();
        Thread t2 = new MyThread("线程2");
        t1.setName("线程1");


        //开启线程
        t1.start();
        t2.start();
    }

image-20240529172022639

3.currentThread()

作用:获取当前线程

使用这个方法,我们可以获取到当前执行的线程,那么我们如果将我们前面的方法都注释掉,获取到的又是什么呢?

image-20240529172955827

我们运行一下

image-20240529173020725

可以看到输出了一个main

这里可以给大家补充一点JVM知识

当JVM虚拟器启动之后,会自动的启动多条线程

其中有一条线程就是main线程

它的作用就是去调用main方法,并去执行里面的代码

所以我们写过的所有代码,都是运行在main线程当中

4.sleep()

作用:让线程睡眠多少时间 单位为毫秒,当时间到后线程会自动的醒来,继续执行其他的代码

我们直接来进行测试,由于这里是延迟测试,不直观,大家可以自行测试

image-20240529173602626

线程优先级问题

接下来的俩个方法,我们先来补充一点知识

线程调度问题

  1. 抢占式调度

    • 解释:指多个线程去抢占CPU的执行权,CPU在执行哪个线程是不确定的,执行时间也是不确定的。
    • 特点:随机性
  2. 非抢占式调度

    • 解释:指多个线程轮流的去执行,每个线程的执行的时间是差不多的

那么在Java中是采取了第一种抢占式调度,所以我们只需要在意俩个字随机,而随机的方式是跟优先级有关,下面俩个方法就是跟优先级有关,优先级越大抢到CPU的概率就是越大的,优先级分为

1-10,1最小10最大,没有设置默认是5

5.getPriority()

作用:获取线程优先级

t1.getPriority())

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们来看看输出的结果

image-20240530095310562

6.setPriority()

作用:设置线程优先级

看了上面的默认线程数量,现在我们来调整优先级,看看抢占情况

//创建线程对象
MyThread mt = new MyThread();
Thread t1 = new Thread(mt,"线程1");
Thread t2 = new Thread(mt,"线程2");

t1.setPriority(1);
t2.setPriority(10);

//开启线程
t1.start();
t2.start();

注意:优先级高并不代表百分百抢占到线程只是概率会优先运行完

image-20240530095800798

这里我就没有过多测试,大家有兴趣可以试一试

守护线程

作用:为其他线程提供服务。通俗来讲就是非守护线程执行完毕后,守护线程也会陆续结束

例如:清空过时的缓存项、计时器线程

或者这里在举一个通俗的例子,我们使用的QQ

image-20240530101319936

我们把聊天窗口当做一个线程,当我们在传输文件的时候,开启了另一个线程,如果此时我把聊天窗口关闭,传输文件的线程也会结束。

上面的传输文件就属于守护线程

前置demo

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

注意两者区别,守护线程我打印的数量会多一点

7.setDaemo()

作用:设置为守护线程

//ThreadDemo3
public class ThreadDemo3 {
    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();

    }
}

我们运行程序,看看效果

image-20240530101203156

出让/礼让线程

作用:顾名思义就是线程执行一次后,让出执行权,再去争夺

前置demo:

//threadDemo
    public static void main(String[] args) throws InterruptedException {
        //创建线程对象
        MyThread1 t1 = new MyThread1();
        MyThread1 t2 = new MyThread1();
        t1.setName("线程1");
        t2.setName("线程2");
        
        //开启线程
        t1.start();
        t2.start();
    }

8.yield()

我们直接上代码

public class MyThread1 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i <=10 ; i++) {
            System.out.println(getName() + "   "+i);
            //让出
            Thread.yield();
        }
    }
}

我们看看运行结果

image-20240530102339325

可以看到基本上都是均匀的每个线程一次执行

插入线程/插队线程

前置demo

public class MyThread1 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i <=10 ; i++) {
            System.out.println(getName() + "   "+i);
            //让出
            Thread.yield();
        }
    }
}

9.join()

作用:插队,提前先执行线程

我们来看demo

    public static void main(String[] args) throws InterruptedException {
        //创建线程对象
        com.methodDemo4.MyThread1 t1 = new com.methodDemo4.MyThread1();
        t1.setName("线程1");

        //开启线程
        t1.start();

        for (int i = 0; i <=10 ; i++) {
            System.out.println("main  " + i);
        }
    }

上面这段程序运行结果,是main线程跟我们创建的线程一起抢夺CPU,结果会发生什么呢

image-20240530102859613

可以看到main线程优先执行完,然后才是我们自己创建的线程,那么我们想把我们自己创建的线程优先执行完呢?这个时候就可以用join方法了

image-20240530103107845

我们在运行一次

image-20240530103118657

线程的生命周期

接下来我们来看看线程的生命周期

image-20240530103345759

线程一共有五个状态

  1. 新建(New):当一个Thread类的实例被创建时,它处于新建状态。此时还没有调用start()方法启动线程。
  2. 可运行(Runnable):线程进入运行状态后,可以通过调用start()方法来启动线程,线程处于可运行状态,并没有被挂起,可能正在执行也可能等待CPU调度。
  3. 阻塞(Blocked):线程在某些情况下会被挂起,如等待I/O操作完成、获取锁等。在这种状态下,线程不会占用CPU资源,直到阻塞条件被解除。
  4. 等待(Waiting):当线程调用wait()方法、join()方法或LockSupport.park()方法时,线程会进入等待状态,直到其他线程调用notify()、notifyAll()方法或join()中的线程执行完毕时,线程才会重新进入可运行状态。
  5. 终止(Terminated):线程执行完任务或者调用stop()方法结束线程时,线程进入终止状态。一旦线程终止,它就不能再进入可运行状态。

线程安全的问题

需求:

某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票

那么我们来写一下这个demo

//MyThread
public class MyThread extends Thread{
    int tickets = 1;

    @Override
    public void run() {
        while(true){
            if (tickets>100){
                break;
            }
            System.out.println("正在卖第  "+tickets+"  张票");
            tickets++;

        }
    }
}

//ThreadDemo6
public class ThreadDemo6 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

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

        //启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

我们运行一下程序看看效果

image-20240530105637004

可以看到如果我们这样设计,他会造成每个窗口都卖了100的现象,那么如果我们将tickets设置成静态变量

image-20240530105820609

在运行一次

image-20240530105918402

可以看到这次又会造成票重复卖的情况,那么这样的情况我们该如何解决呢

同步

经过上面的练习,我们可以看到俩个或俩个以上的线程需要共享对同一个数据的存取时,俩个线程会相互覆盖,导致多线程破坏共享数据,接下来我们就来学习如何解决这个问题

synchronized()

同步代码块

同步代码块:在代码中把操作共享的数据的代码锁起来

注意点:

​ 括号内需要传入一个唯一的锁对象,例如Object、当前文件字节码文件对象例如MyThread.class,如果不唯一则锁失效

特点:

  1. 默认打开,有一个线程进去了,锁自动关闭
  2. 里面的代码全部执行完毕,线程出来,锁自动打开

接下来我们改变一下代码

    static int tickets = 1;

    //锁对象,一定要是唯一
    static  Object obj = new Object();

    @Override
    public void run() {
        while(true){
            synchronized (obj){
                if (tickets>100){
                    break;
                }
                try {
                    System.out.println(getName()+  "正在卖第  "+tickets+"  张票");
                    tickets++;
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

然后我们运行查看结果

image-20240530111952482

可以看到这下就不会出现超卖的情况

同步方法

作用:将方法锁起来

image-20240530112522322

特点:

  1. 同步方法是锁住方法里面所有的代码
  2. 锁对象不能是自己指定
    • 非静态:this
    • 静态:当前类的字节码文件

接下来我们用Runnable实现

//ThreadDemo
public class ThreadDemo7 {
    public static void main(String[] args) {
        //创建任务
        MyThread mt = new MyThread();

        //创建线程
        Thread t1 = new Thread(mt);
        Thread t2 = new Thread(mt);
        Thread t3 = new Thread(mt);

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

        //启动
        t1.start();
        t2.start();
        t3.start();
    }
}
//MyThread
public class MyThread implements Runnable{
    int tickets = 0;

    @Override
    public void run() {
        while (true){
            if (method()){
                break;
            }
        }
    }

    //锁对象为方法
    private synchronized   boolean  method()  {
        if (tickets==100){
           return true;
        }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            tickets++;
     System.out.println(Thread.currentThread().getName()+" 正在卖 "+ tickets+" 张票");

        return false;
    }
}

注意这里变化tickets没有静态,因为我们Runable是个唯一作为任务传入Thread

image-20240530172745826

Lock锁

看了上面的方法,其实我们并没有理解到在哪里加上了锁和在哪释放了锁,所以下面我们来看看新的锁对象Lock

方法:

  • void lock() 获得锁
  • viod unlock() 释放锁

注意:Lock是接口不能直接实例化,这里采用了它的实现类ReentrantLock()来实例化

我们来看demo

// ThreadDemo8
public class ThreadDemo8 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

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

        //启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

//MyThread
public class MyThread extends Thread{
    static int tickets = 1;
    //创建锁对象
    static Lock lock =   new ReentrantLock();
    @Override
    public void run() {
        while(true){
            try {
                //加锁
                lock.lock();
                if (tickets>100){
                    break;
                }else {
                    Thread.sleep(10);
                    System.out.println(getName()+  "正在卖第  "+tickets+"  张票");
                    tickets++;
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }finally {
                lock.unlock();
            }

        }
    }
}

主要是修改了三处地方,第一个就是创建锁对象,第二步就是在需要的地方加锁,第三处就是释放锁,利用finally保证锁能够被释放,不会造成锁没有释放

image-20240530183237026

死锁

关于死锁的原因:给大家举一个例子即可

假设有两个人,Alice 和 Bob,他们共享着两个资源,一个是打印机,另一个是扫描仪。他们都需要先打印一份文件,然后扫描该打印出来的文件。

现在,Alice 拿到了打印机,开始打印她的文件。与此同时,Bob 拿到了扫描仪,开始扫描他的文件。但是,Alice 在打印完文件后想要扫描,而 Bob 在扫描完文件后想要打印。由于他们各自持有对方需要的资源,因此他们无法继续进行操作,陷入了死锁状态。

简单来说 就是A等B B等A 然后就卡死了

不要进行锁嵌套

public class ResourceSharing {

    public static void main(String[] args) {
        final Object printer = new Object();
        final Object scanner = new Object();

        // Alice线程
        Thread alice = new Thread(() -> {
            synchronized (printer) {
                System.out.println("Alice is printing.");
                try {
                    Thread.sleep(100); // 模拟打印时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (scanner) {
                    System.out.println("Alice is scanning.");
                }
            }
        });

        // Bob线程
        Thread bob = new Thread(() -> {
            synchronized (scanner) {
                System.out.println("Bob is scanning.");
                try {
                    Thread.sleep(100); // 模拟扫描时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (printer) {
                    System.out.println("Bob is printing.");
                }
            }
        });

        alice.start();
        bob.start();
    }
}

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

生产者消费者模式是一个十分经典的多线程协作的模式

正常情况下,我们在运行俩条线程的时候,顺序一般是随机的

image-20240530190517386

而在该模式下 ,会让线程轮流执行

image-20240530190547335

消费者等待

image-20240530190858433

关于消费者等待就如上图所示,如果桌上没有东西,消费者就会等待(wait)然后释放掉CPU的执行权,此时一定是生产者抢到CPU做好后,利用唤醒(notify)消费者

生产者等待

image-20240530191214236

如上图,如果是生产者抢到CPU,此时桌子上没有面条,生产者做好面条,释放掉了CPU的执行权,然后生产者又抢到了CPU的执行权,但桌上已经有了面条,厨师只能等待(wait),然后消费者就会抢到

等待唤醒机制

那么完整的机制就是如下图

image-20240530191547698

生产者和消费者在执行前都会先去判断桌上是否有食物,消费者发现有则开吃,生产者发现没有就开始制作

常见方法

image-20240530191750529

那么我们来看看Demo

下面会创建四个类

  1. Cook 厨师
  2. Foodie 食客
  3. Desk 桌子
    • 控制消费者和生产者
  4. ThreadDemo 主方法
    • 实现线程轮流交替执行
//Desk 
public class Desk {
    /**
     * 公共变量,控制Cook 和 Foodie
     */

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

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

    //是否有面条 1:有面条 0:无面条
    public static int foodFlag = 0;
    
}

//Cook
public class Cook  extends  Thread{
    /**
     * 生产食物
     */

    @Override
    public void run() {
        while (true){
            synchronized (Desk.lock){
                if (Desk.count==0){
                    break;
                }else {
                    //判断桌子上是否有食物
                    if (Desk.foodFlag == 0){
                        //没有就开始生产
                        Desk.foodFlag = 1;
                        System.out.println("厨师已经生产了一个食物");
                        //让线程跟锁进行绑定
                        //唤醒该锁下面的全部线程
                        Desk.lock.notifyAll();
                    }else {
                        //有食物就开始等待
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
            }
        }
    }
}

//Foodlie
public class Foodlie extends Thread {
    @Override
    public void run() {
        while (true) {
            //创建同步代码块
            synchronized (Desk.lock) {
                //判断还能吃多少碗
                if (Desk.count == 0) {
                    break;
                } else {
                    //判断桌上是否有食物
                    if (Desk.foodFlag == 0) {
                        //没有则等待
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }else {
                        //有则开吃
                        Desk.count--;
                        System.out.println("还能吃 "+Desk.count+" 碗");
                        //将桌子状态调整为没有食物
                        Desk.foodFlag = 0;
                        //唤醒厨师
                        Desk.lock.notifyAll();
                    }
                }
            }
        }

    }
}

//ThreadDemo
public class ThreadDemo {
    public static void main(String[] args) {
        Foodlie f = new Foodlie();
        Cook c = new Cook();

        //启动线程
        f.start();
        c.start();

    }
}

代码解释:

Desk.lock.wait();

Desk.lock.notifyAll();

以上代码是让线程跟锁进行绑定,可以让使用同一个锁的线程等待或唤醒

运行结果

image-20240530194921914

等待唤醒机制(阻塞队列方式实现)

image-20240530195647456

跟上图所示,我们利用阻塞队列的方式,将食物放在队列中,队列的特点先进先出,所以我们消费者可以依次拿取

这里需要注意

  1. 生产者和消费者必须处于同一个阻塞队列

阻塞队列的继承结构

image-20240530195724706

阻塞队列一共以上实现了四个接口,可以看到阻塞队列我们可以利用迭代器或者增强for来进行遍历,我们要创建的是下面俩个实现类

image-20240530195902897

那么我们接下来来看看实现Demo

//Foodie
public class Foodie extends Thread{
    ArrayBlockingQueue<String> arrayBlockingQueue ;

    public Foodie(ArrayBlockingQueue arrayBlockingQueue) {
        this.arrayBlockingQueue = arrayBlockingQueue;
    }

    @Override
    public void run() {
        while (true){
            //判断食物数量
            if (Desk.count==0){
                break;
            }else {
                try {
                    String food = arrayBlockingQueue.take();
                    System.out.println(food + "  "+Desk.count);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}
//Cook
public class Cook extends Thread{
    ArrayBlockingQueue<String> arrayBlockingQueue ;

    public Cook(ArrayBlockingQueue arrayBlockingQueue) {
        this.arrayBlockingQueue = arrayBlockingQueue;
    }

    @Override
    public void run() {
        while (true){
            try {
                //判断食物数量
                if (Desk.count==0){
                    break;
                }else {
                    arrayBlockingQueue.put("面条");
                    Desk.count--;
                    System.out.println("厨师放了一碗面条");
                }

            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
//Desk
public class Desk {
    //食物数量
    public static int count = 10;
}
//ThreadDemo10
public class ThreadDemo10 {
    public static void main(String[] args) {
        //每次只能通过一个食物
        ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1) ;
        Cook cook = new Cook(arrayBlockingQueue);
        Foodie foodie = new Foodie(arrayBlockingQueue);

        cook.start();
        foodie.start();
    }
}

这里解释一下,为啥没有锁关键字,其实已经在队列方法中中创建了

image-20240530201617528

然后这里的队列是由我们传递进去的通过构造方法,确保俩个线程都使用同一个队列

image-20240530201721941

然后我们来看看运行结果

image-20240530201740017

注意这里运行结果为啥会出现俩个,是因为我们锁是在队列中,而不是锁住的整个方法或者代码块,所以打印语句可能会出现多次打印情况,但是运行是不影响的。不可以在加个锁进行嵌套否则会出现死锁

线程状态

image-20240530201937266

注意这里Java中是没有运行状态的,原因是因为抢线程抢到CPU后JVM虚拟机将当前线程交给操作系统管理

但是在八股文中主要是六种状态(这里参照的是Java核心卷I):

  1. New(新建)----------------> 创建线程
  2. Runnable(可运行)----------------> start方法
  3. Blocked(阻塞)----------------> 无法获得锁对象
  4. Waiting(等待)----------------> wait方法
  5. Timed waiting(计时等待)----------------> sleep方法
  6. Terminated(终止)----------------> 全部代码运行完毕

线程池

概念

理解线程池概念,我们先来看看一张图

image-20240530203054247

这个碗就是我们的线程A,当我们用的时候,我们就去买一个碗

然后我们吃完饭后就把碗摔掉(线程消失)

当我们又需要吃饭的时候怎么办呢?我们又要去买

image-20240530203238085

如此重复,造成大量的浪费。那么我们怎么解决这个问题呢

image-20240530203314635

我们弄一个碗柜(线程池),这样每次吃完我们就把碗放回到碗柜中,就不会浪费碗了

那么接下来我们来看看其他效果图帮助我们更好的理解

image-20240530203557067

可以看上面这张图,我们创建了一个线程池,最大线程数量为3

此时有五个任务需要执行,但是线程就只有三个,后俩个只能等着

核心原理

  1. 创建一个池子,池子中是空的
  2. 提交任务的时候,池子会创建新的线程池对象,任务执行完毕,线程会归还给池子,下次在提交任务,也不需要新的线程,直接复用已有的线程即可
  3. 但是如果提交任务,池子中没有空闲的线程,也无法创建新的线程,任务就会排队等待

代码实现

Executors:

线程池的工具类通过调用方法返回不同类型的线程池对象。

image-20240530203947615

注意第一个也是有上线的,是Int的最大数,但是相信没有电脑能抗住这么多个线程数。

image-20240530204500079

线程池只能接受Callable 或者 Runnable

newCachedThreadPool()

那么我们来看实现Demo

//MyRunnable
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 1; i < 100 ; i++) {
            System.out.println(Thread.currentThread().getName() + "   " + i);
        }
    }
}
//MyThread
public class MyThread {
    public static void main(String[] args) {
        //创建线程池
        ExecutorService pool = Executors.newCachedThreadPool();

        //提交任务
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());

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

注意这里的销毁线程池一般是不用的,如果销毁了整个线程池就没有了

image-20240530204759882

可以看到变化,如果大家想看到线程复用的效果只需要在每次提交后休眠几秒

image-20240530205001340

image-20240530205027079

即可看到效果

image-20240530205056926

可以看到任务都是线程1执行的

newFixedThreadPool()

创建该线程需要指定线程最大数量,其他代码可以不用更改

image-20240530205328037

大家可以运行一下看看是不是只有3个线程在运行

image-20240530205419939

我们可以Debug运行一下

image-20240530205540651

image-20240530205703931

可以看如上图,我们代码运行完第三个 活动线程已经达到最大三个,等待任务还没有出现,我们继续运行

image-20240530210022152

可以看到已经有一个任务在等待了

自定义线程池

Java给我们提供的创建线程方式是方便了我们使用,但是不够灵活,例如我们排队时间过多时,我们想拒绝访问,是无法实现的

同样我们来看一组图片,来方便我们自定义线程池

image-20240530210717222

当我们开一家高级饭店,进行一对一服务的时候,不可能来多少桌就找多少人,也不可能一直没人,所以我们规定有正式员工和临时工,就如同外包好吧。

那么如果餐厅如果生意太好,我们无法接待那么多,也需要有规则

image-20240530210930766

比如在箭头处就让其他顾客回家,不要继续等待,这就是一个线程池的使用

image-20240530211843589

同样我们来看下面这张图

image-20240530211355993

这里需要我们注意的点

  1. 临时线程不会第一时间创建,必须要等队伍长度达到上线,才会创建临时线程
  2. 任务执行顺序不会根据我们提交的顺序提交,可能后提交的先被执行完
  3. 如果提交任务超过了 核心线程+ 临时线程+队伍长度,就会触发拒绝策略

拒绝策略

代码实现

//myRunnable
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 1; i < 100 ; i++) {
            System.out.println(Thread.currentThread().getName() + "   " +i );
        }
    }
}
//MyThread2
public class MyThread2 {

    public static void main(String[] args) {
        ThreadPoolExecutor pool =  new ThreadPoolExecutor(
                3,//核心线程数
                6,//最大线程数
                60,//最大存活时间
                TimeUnit.SECONDS,//时间单位
                new ArrayBlockingQueue<>(3),//指定队伍长度
                Executors.defaultThreadFactory(),//创建线程工厂
                new ThreadPoolExecutor.AbortPolicy()//拒绝策略
        );

        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());
    }
}

运行结果大家自行测试就行

  • 21
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

周粥粥ya

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

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

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

打赏作者

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

抵扣说明:

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

余额充值