Java多线程

1.实现多线程

1.1简单了解多线程【理解】

是指从软件或者硬件上实现多个线程并发执行的技术。 具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。

1.2并发和并行【理解】

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

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

1.3进程和线程【理解】

  • 进程:是正在运行的程序

    独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位 动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的 并发性:任何进程都可以同其他进程一起并发执行

  • 线程:是进程中的单个顺序控制流,是一条执行路径

    单线程:一个进程如果只有一条执行路径,则称为单线程程序

    多线程:一个进程如果有多条执行路径,则称为多线程程序

1.4实现多线程的方式一:继承Thread类的方式【应用】

  • 方法介绍

    方法名 说明
    void run() 在线程开启后,此方法会被调用执行
    void start() 使此线程开始执行,Java虚拟机会调用run方法()
  • 实现步骤

    • 自定义一个类MyThread继承Thread类并重写其run()方法

    • 创建MyThread类的对象,并使用它调用start()方法启动线程

代码演示:

Mythread类:

java 复制代码
package com.mythread.a01;

public class Mythread extends Thread{
    @Override
    public void run() {
        //编写线程要执行的代码
        for (int i = 0;i<100;i++){
            System.out.println(getName() + "HelloWorld!");
        }
    }
}

有mian方法的类:

java 复制代码
package com.mythread.a01;

public class ThreadDemo {
    public static void main(String[] args) {
        /**
         * 多线程的第一种启动方式:
         *  1.定义一个类继承Thread类并重写其run方法
         *  2.创建子类的对象,并启动线程
         */
        Mythread t1 = new Mythread();
        Mythread t2 = new Mythread();

        //给线程起个名字
        t1.setName("线程1");
        t2.setName("线程2");

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


    }
}

运行后可以看到两个线程交替执行:

  • 两个小问题

    • 为什么要重写run()方法?

      因为run()是用来封装被线程执行的代码

    • run()方法和start()方法的区别?

      run():封装线程要执行的代码,直接调用,相当于普通方法的调用

      start():启动线程,然后由JVM调用此线程的run()方法

1.5 实现多线程的方式二:实现Runnable接口的方式【应用】

  • Thread构造方法

    方法名 说明
    Thread(Runnable target) 分配一个新的Thread对象
    Thread(Runnable target, String name) 分配一个新的Thread对象
  • 实现步骤

    • 定义一个类实现Runnable接口并重写其run方法

    • 创建Runnable接口的实现类的对象,表示线程要执行的任务

    • 创建一个Thread类的对象并传入一个Runnable实例(表示线程)

    • 使用Thread对象,调用其start()方法启动线程

  • 代码演示

MyRun类:

java 复制代码
package com.mythread.a02;

public class MyRun implements Runnable{
    @Override
    public void run() {
        //线程要执行的代码
        for (int i = 0; i < 100; i++) {
            //获取当前线程的对象
            Thread t = Thread.currentThread();
            System.out.println(t.getName() + "HelloWorld");
        }
    }
}

有main方法的类:

java 复制代码
package com.mythread.a02;

public class ThreadDemo {
    public static void main(String[] args) {
        /**
         * 多线程的第二种实现方式
         *  1.定义一个类实现Runnable接口并重写其run方法
         *  2.创建实现了Runnable接口的类的对象
         *  3.创建一个Thread类的对象,并开启线程
         */

        //创建MyRun对象,表示多线程要执行的任务
        MyRun mr = new MyRun();

        //创建线程对象,传入一个Runnable实例
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);

        //给线程设置名字
        t1.setName("线程1");
        t2.setName("线程2");

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

    }
}

运行main方法后,可以看到t1和t2线程交替执行:

1.6实现多线程方式三: 实现Callable接口的方式【应用】

  • 实现步骤

    • 创建一个类实现Callable接口并重写call方法(这个方法的返回值就是多线程运行的结果)

    • 创建Callable接口的实现类对象(表示多线程要执行的任务)

    • 创建Future的实现类FutureTask对象,把Callable接口的实现类对象作为构造方法的参数

    • 创建Thread类的对象,把FutureTask对象作为构造方法的参数,并启动(表示线程),

    • 再调用get方法,就可以获取线程结束之后的结果。

  • 代码演示

MyCallable类:

java 复制代码
package com.mythread.a03;

import java.util.concurrent.Callable;

//Callable接口的泛型用于执行多线程执行的结果是什么类型的
public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        //求1到100之间的和
        int sum = 0;
        for (int i = 0; i <= 100; i++) {
            sum = sum + i;
        }
        return sum;
    }
}

有main方法的类:

java 复制代码
package com.mythread.a03;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /**
         * 多线程的第三种实现方式:
         *  特点:可以获取到多线程的运行结果
         *  1.创建一个类实现Callable接口并重写call方法(这个方法的返回值就是多线程运行的结果)
         *  2.创建实现了Callable接口的类的对象(表示多线程要执行的任务)
         *  3.创建FutureTask的对象(管理多线程运行的结果)
         *  4.创建Thread类的对象,并启动(表示线程)
         */

        //创建实现了Callable接口的类的对象(表示多线程要执行的任务)
        MyCallable mc = new MyCallable();

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

        //创建线程的对象,并传入要执行任务的实例
        Thread t1 = new Thread(ft);
        //启动线程
        t1.start();

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

}

运行结果:

三种实现方式的对比

  • 实现Runnable、Callable接口

    • 好处: 扩展性强,实现该接口的同时还可以继承其他的类

    • 缺点: 编程相对复杂,不能直接使用Thread类中的方法

  • 继承Thread类

    • 好处: 编程比较简单,可以直接使用Thread类中的方法

    • 缺点: 可以扩展性较差,不能再继承其他的类

Thread中常见的成员方法

getName和setName方法:
java 复制代码
package com.mythread.a04;

public class MyThread extends Thread{

    /**
     * 因为Mythread类是Thread类的子类,而构造方法是不会继承的,
     * 所以需要在子类中显示定义构造方法,再去里面调用父类的构造方法
     */
    public MyThread() {
    }

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

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + "@" + i);
        }
    }
}
java 复制代码
package com.mythread.a04;


/*
    String getName()   获取当前线程的名称
        细节:Thread类的构造方法也可以设置线程的名字
    void setName(String name)  设置此线程的名字
        细节:如果没有给线程设置名字,线程的名字默认为Thread-X(X是序号,从0开始)

 */


public class ThreadDemo {
    public static void main(String[] args) {

        MyThread t1 = new MyThread("飞机");
        MyThread t2 = new MyThread("坦克");

        t1.start();
        t2.start();
    }
}
currentThread方法:
java 复制代码
package com.mythread.a04;


/*
    static Thread currentThread()  获取当前线程的对象
        细节:当JVM虚拟机启动后,会自动启动多条线程
        其中有一条线程就叫做main线程
        它的作用就是去调用main方法,并执行里面的代码
        所以,我们写的所有代码,都是运行在main线程中的

 */


public class ThreadDemo {
    public static void main(String[] args) {
        //哪个线程执行到这个方法,此时获取的就是那个线程的对象
        Thread t = Thread.currentThread();
        System.out.println(t.getName());
    }
}

运行结果:

static void sleep(long time)方法:

实例1:

java 复制代码
package com.mythread.a04;


/*
    static void sleep(long time)  让线程休眠指定的时间,单位为毫秒
        细节:
            1.哪条线程执行到这个方法,哪条线程就会在这里停留对应的时间
            2.当时间到了之后,线程就自动醒来,继续执行下面的其他代码

 */


public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("111111111111");
        Thread.sleep(5000); //main线程执行到这里后停留了5秒
        System.out.println("222222222");
    }
}

实例2:

java 复制代码
package com.mythread.a04;

public class MyThread extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(getName() + "@" + i);
        }
    }
}
java 复制代码
package com.mythread.a04;


/*
    static void sleep(long time)  让线程休眠指定的时间,单位为毫秒
        细节:
            1.哪条线程执行到这个方法,哪条线程就会在这里停留对应的时间
            2.当时间到了之后,线程就自动醒来,继续执行下面的其他代码

 */


public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

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

1.9 线程优先级【应用】

  • 线程调度

    • 两种调度方式

      • 分时调度模型(非抢占式调度模型):所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片

      • 抢占式调度模型:优先级高的线程会大概率抢占到 CPU的使用权,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些

      • Java使用的是抢占式调度模型

    • 随机性

      • 假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性的,因为谁抢到CPU的使用权是不一定的

代码演示:

java 复制代码
package com.mythread.a05;

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName());
        }
    }
}
java 复制代码
package com.mythread.a05;

public class ThreadDemo {
    /*
        setpriority(int newPriority)  设置线程的优先级

        final int getPriority   获取线程的优先级
     */
    public static void main(String[] args) {
        //创建线程要执行的参数对象
        MyRunnable mr = new MyRunnable();
        //创建线程对象
        Thread t1 = new Thread(mr, "飞机");
        Thread t2 = new Thread(mr,"坦克");

//线程默认的优先级都是为:5(最大为10,最小为1)
//        System.out.println(t1.getPriority());
//        System.out.println(t2.getPriority());
//        System.out.println(Thread.currentThread().getPriority());

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

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

1.10 守护线程【应用】

java 复制代码
package com.mythread.a06;

public class MyThread1 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName() + "@" + i);
        }
    }
}
java 复制代码
package com.mythread.a06;

public class MyThread2 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + "@" + i);
        }
    }
}
java 复制代码
package com.mythread.a06;

public class ThreadDemo {
    /*
        final void setDaemon(boolean on)  设置为守护线程
            细节:当其他的非守护线程执行完之后,守护线程就会慢慢结束

     */
    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();
    }
}

运行结果:

可以看到,当女神线程执行完之后,备胎线程就会慢慢结束,并不会执行到备胎@100

1.11 线程的生命周期

新建状态:当创建线程对象的时候

就绪状态:创建好线程对象后,调用完start()方法后变为就绪状态

  • 线程可以抢夺cpu的使用权,但可能还没抢到,不能去执行代码(有执行资格,但没有执行权)

运行状态:当就绪状态的线程抢到cpu的执行权后,就会变成运行状态,线程开始运行代码

  • 在运行过程中,cpu可能会被其他线程抢走,又会变成就绪状态(有执行资格,也有执行权)

死亡状态:run方法里的代码都运行完后,线程就会死亡

阻塞状态:在运行过程中,遇到sleep方法,线程就会变为阻塞状态,当sleep的时间到或其他阻塞方式结束,线程就会变成就绪状态(没有执行资格,也没有执行权)

1.12 解决线程安全问题

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

java 复制代码
package com.mythread.a09;

public class MyThread extends Thread{
    int ticket = 0;

    @Override
    public void run() {
        while (true){
            if (ticket < 100){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                ticket++;
                System.out.println("正在卖第" + ticket + "张票!!!");
            }else {
                break;
            }
        }
    }
}
java 复制代码
package com.mythread.a09;

public class ThreadDemo {
    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();
    }
}

此时运行代码,结果是不正确的,卖了300张票,每个线程都买了相同的票一次:

修改MyThread类,给变量ticket加上static关键字:

java 复制代码
package com.mythread.a09;

public class MyThread extends Thread{
    //加上static关键字,表示这个类的所有对象共享ticket变量
    static int ticket = 0;

    @Override
    public void run() {
        while (true){
            if (ticket < 100){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                ticket++;
                System.out.println("正在卖第" + ticket + "张票!!!");
            }else {
                break;
            }
        }
    }
}

结果还是不对:

买票引发的线程安全问题:

  1. 相同的票出现了多次
    1. 原因:线程在执行的时候具有随机性,cpu的执行权可能随时会被其他线程抢走
  2. 出现了超范围的票
    1. 原因:线程在执行的时候具有随机性,cpu的执行权可能随时会被其他线程抢走

解决:将操作共享数据的代码锁起来,当有线程1进来后,锁关闭,其他线程就算抢到cpu的执行权,也得等着,只有当线程1执行完,锁打开,其他线程才能进去

同步代码块

语法:使用sychronized关键字

java 复制代码
synchronized(锁对象){  //可以使用任意一个对象作为锁对象,但必须唯一
    操作共享数据的代码
}

特点:

  • 锁默认打开,当有一个线程进去了,锁自动关闭
  • 锁里面的代码都执行完了,线程出来,锁自动打开

修改MyThread类,用同步代码块将操作共享数据的代码锁起来:

java 复制代码
package com.mythread.a09;

public class MyThread extends Thread{
    //加上static关键字,表示这个类的所有对象共享ticket变量
    static int ticket = 0;

    //创建一个Object对象作为锁对象,一定要唯一
    static Object obj = new Object();

    @Override
    public void run() {
        while (true){
            //这种处理方式叫做同步代码块
            synchronized (obj){
                if (ticket < 100){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    ticket++;
                    System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
                }else {
                    break;
                }
            }
        }
    }
}

结果正确,最后顺序卖了100张票:

注意点:锁对象必须唯一,不然就锁不起来代码了,因为每个线程对应不同的锁,如下修改MyThread类,将obj对象改为this:

  • this:当哪个线程进来后,this就是哪个线程对象,此时锁对象不同
java 复制代码
package com.mythread.a09;

public class MyThread extends Thread{
    //加上static关键字,表示这个类的所有对象共享ticket变量
    static int ticket = 0;

    //创建一个Object对象作为锁对象,一定要唯一
//    static Object obj = new Object();

    @Override
    public void run() {
        while (true){
            //这种处理方式叫做同步代码块
            synchronized (this){
                if (ticket < 100){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    ticket++;
                    System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
                }else {
                    break;
                }
            }
        }
    }
}

运行结果,又有重复票,有会超出票的范围:

一般将锁对象写成Thread.class对象,因为字节码对象是唯一的:yi

java 复制代码
package com.mythread.a09;

public class MyThread extends Thread{
    //加上static关键字,表示这个类的所有对象共享ticket变量
    static int ticket = 0;

    @Override
    public void run() {
        while (true){
            //这种处理方式叫做同步代码块
            synchronized (MyThread.class){
                if (ticket < 100){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    ticket++;
                    System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
                }else {
                    break;
                }
            }
        }
    }
}
同步方法

把synchronized关键字加到方法上

格式:

java 复制代码
修饰符 synchronized 返回值类型 方法名(形参表){...}

特点:

  • 同步方法是锁住方法里面所有的代码
  • 锁对象不能自己指定
    • 如果当前的方法是非静态方法,锁对象就是this,也就是当前方法的调用者
    • 如果当前的方法是静态方法,锁对象就是当前类的字节码文件对象

利用同步方法完成1.12开始的那个需求:

  • 技巧,先写成同步代码块,然后再把同步代码块里面的那部分内容抽取成方法
java 复制代码
package com.mythread.a10;

public class MyRunnable implements Runnable{

    int ticket= 0;

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

    //此时锁对象是this,也就是mr对象
    private synchronized boolean method() {
        if (ticket == 100){
            return true;
        }else {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            ticket++;
            System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票!!!");
        }
        return false;
    }
}
java 复制代码
package com.mythread.a10;

public class ThreadDemo {
    public static void main(String[] args) {

        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();
    }
}

1.13 Lock锁

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

Lock接口中提供了手动获得锁和释放锁的方法:

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

注意:Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化Lock对象。使用ReentrantLock类的无参构造方法来实例化一个Lock锁对象。

实例:

java 复制代码
package com.mythread.a11;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyThread extends Thread{
    //加上static关键字,表示这个类的所有对象共享ticket变量
    static int ticket = 0;

    //创建Lock锁对象
    //加上static关键字,表示这个类的所有对象共享同一个锁对象lock
    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
            //这种处理方式叫做同步代码块
//            synchronized (MyThread.class){
            lock.lock();
            try {
                if (ticket == 100){
                    break;
                }else {
                    Thread.sleep(10);
                    ticket++;
                    System.out.println(getName() + "在卖第" + ticket + "张票");
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }

//            }
        }
    }
}
java 复制代码
package com.mythread.a11;

public class ThreadDemo {
    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();
    }
}

2.6 死锁【理解】

  • 概述

    线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行

  • 什么情况下会产生死锁

    1. 资源有限

    2. 两个锁嵌套

  • 代码演示

java 复制代码
package com.itheima.a12deadlock;

public class MyThread extends Thread {

    static Object objA = new Object();
    static Object objB = new Object();

    @Override
    public void run() {
        //1.循环
        while (true) {
            if ("线程A".equals(getName())) {
                synchronized (objA) {
                    System.out.println("线程A拿到了A锁,准备拿B锁");//A
                    synchronized (objB) {
                        System.out.println("线程A拿到了B锁,顺利执行完一轮");
                    }
                }
            } else if ("线程B".equals(getName())) {
                if ("线程B".equals(getName())) {
                    synchronized (objB) {
                        System.out.println("线程B拿到了B锁,准备拿A锁");//B
                        synchronized (objA) {
                            System.out.println("线程B拿到了A锁,顺利执行完一轮");
                        }
                    }
                }
            }
        }
    }
}
java 复制代码
package com.itheima.a12deadlock;


public class ThreadDemo {
    public static void main(String[] args) {
       /*
           需求:
                死锁
       */


        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        t1.setName("线程A");
        t2.setName("线程B");

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

    }
}

3.生产者消费者

3.1生产者和消费者模式概述【应用】

  • 概述

    生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者-消费者问题能够让我们对多线程编程的理解更加深刻。

    所谓生产者消费者问题,实际上主要是包含了两类线程:

    一类是生产者线程:用于生产数据

    一类是消费者线程:用于消费数据

    为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库

    生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为

    消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为

  • Object类中的等待和唤醒方法(一般是Object对象作为锁对象的时候用)

    方法名 说明
    void wait (这个锁)让当前线程等待,直到另一个线程调用该锁对象的 notify()方法或 notifyAll()方法
    void notify() 随机唤醒一个正在等待(这个锁)的线程
    void notifyAll() 唤醒所有正在等待(这个锁)的线程

3.2生产者和消费者案例【应用】

第一种情况:消费者先等待

第二种情况:完整的情况

代码演示:

java 复制代码
package com.itheima.a13waitandnotify;

public class Desk {

    /*
    * 作用:控制生产者和消费者的执行
    *
    * */

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

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

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


}
java 复制代码
package com.itheima.a13waitandnotify;

public class Foodie extends Thread{

    @Override
    public void run() {
        /*
        * 1. 循环
        * 2. 同步代码块
        * 3. 判断共享数据是否到了末尾(到了末尾)
        * 4. 判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
        * */

        while(true){
            synchronized (Desk.lock){
                if(Desk.count == 0){
                    break;
                }else{
                    //先判断桌子上是否有面条
                    if(Desk.foodFlag == 0){
                        //如果没有,就等待
                        try {
                            Desk.lock.wait();//让当前线程跟锁进行绑定
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }else{
                        //把吃的总数-1
                        Desk.count--;
                        //如果有,就开吃
                        System.out.println("吃货在吃面条,还能再吃" + Desk.count + "碗!!!");
                        //吃完之后,唤醒厨师继续做
                        Desk.lock.notifyAll();    //唤醒跟这把锁绑定的所有线程
                        //修改桌子的状态
                        Desk.foodFlag = 0;
                    }
                }
            }
        }
    }
}
java 复制代码
package com.itheima.a13waitandnotify;

public class Cook extends Thread{

    @Override
    public void run() {
        /*
         * 1. 循环
         * 2. 同步代码块
         * 3. 判断共享数据是否到了末尾(到了末尾)
         * 4. 判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
         * */

        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();
                    }
                }
            }
        }
    }
}
java 复制代码
package com.itheima.a13waitandnotify;


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();









    }
}

3.3 利用阻塞队列完成生产者和消费者(等待唤醒机制)

消费者从阻塞队列里面取,生产者也从阻塞队列里面放

消费者每次取都是取最先放进去的

注意点:生产者和消费者必须使用同一个阻塞队列

阻塞队列的继承结构:

  • 常见BlockingQueue:

    ArrayBlockingQueue: 底层是数组,有界,创建的时候需要指定队列的长度

    LinkedBlockingQueue: 底层是链表,无界.但不是真正的无界,最大为int的最大值,,创建的时候不需要指定队列的长度

代码实现:

java 复制代码
package com.itheima.a14waitandnotify;

import java.util.concurrent.ArrayBlockingQueue;

public class Cook extends Thread{

    ArrayBlockingQueue<String> queue;

    public Cook(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true){
            //不断的把面条放到阻塞队列中
            try {
                //ArrayBlockingQueue的put方法底层已经有锁,所以不必再写锁
                queue.put("面条");  //调用ArrayBlockingQueue的put方法存入一个元素
                System.out.println("厨师放了一碗面条");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
java 复制代码
package com.itheima.a14waitandnotify;

import java.util.concurrent.ArrayBlockingQueue;

public class Foodie extends Thread{

    ArrayBlockingQueue<String> queue;

    public Foodie(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true){
            //不断从阻塞队列中获取面条
            try {
                //ArrayBlockingQueue的take方法底层已经有锁,所以不必再写锁
                String food = queue.take();//调用ArrayBlockingQueue的put方法存入一个元素
                System.out.println(food);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
java 复制代码
package com.itheima.a14waitandnotify;


import java.util.concurrent.ArrayBlockingQueue;

public class ThreadDemo {
    public static void main(String[] args) {
       /*
       *
       *    需求:利用阻塞队列完成生产者和消费者(等待唤醒机制)的代码
       *    细节:
       *           生产者和消费者必须使用同一个阻塞队列
       *
       * */

        //1.创建阻塞队列的对象
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);

        //2.创建线程的对象,并把阻塞队列传递过去
        Cook cook = new Cook(queue);
        Foodie foodie = new Foodie(queue);

        //3.开启线程
        cook.start();
        foodie.start();


    }
}

4. 线程池

1.1 线程状态介绍

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态。那么Java中的线程存在哪几种状态呢?Java中线程的状态被定义在了java.lang.Thread.State枚举类中,State枚举类的源码如下:

java 复制代码
public class Thread {
    
    public enum State {
    
        /* 新建 */
        NEW , 

        /* 可运行状态 */
        RUNNABLE , 

        /* 阻塞状态 */
        BLOCKED , 

        /* 无限等待状态 */
        WAITING , 

        /* 计时等待 */
        TIMED_WAITING , 

        /* 终止 */
        TERMINATED;
    
	}
    
    // 获取当前线程的状态
    public State getState() {
        return jdk.internal.misc.VM.toThreadState(threadStatus);
    }
    
}

各个状态的转换,如下图所示:

1.2 线程池-基本原理

概述 :

提到池,大家应该能想到的就是水池。水池就是一个容器,在该容器中存储了很多的水。那么什么是线程池呢?线程池也是可以看做成一个池子,在该池子中存储很多个线程。

线程池存在的意义:

系统创建一个线程的成本是比较高的,因为它涉及到与操作系统交互,当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程对系统的资源消耗有可能大于业务处理是对系统资源的消耗,这样就有点"舍本逐末"了。针对这一种情况,为了提高性能,我们就可以采用线程池。线程池在启动的时,会创建大量空闲线程,当我们向线程池提交任务时,线程池就会启动一个线程来执行该任务。等待任务执行完毕以后,线程并不会死亡,而是再次返回到线程池中称为空闲状态。等待下一次任务的执行。

线程池的主要核心原理(重要理解):

1.创建一个线程池,线程池中是空的

2.提交任务时,线程池会创建新的线程对象,当任务执行完毕后,线程会回到线程池中,下次再提交任务时,就不需要创建新的线程,直接复用已有的线程即可

3.如果提交任务时,线程池中没有空闲线程,也无法创建新的线程,任务就会排队等待

线程池的代码实现:

1.创建线程池

2.给线程池提交任务

  • 提交任务的时候,线程池底层会创建线程或复用已存在的线程,不需要我们去做这个动作

3.所有的任务执行完毕后,就可以关闭线程池,但实际生产中,不会去关闭线程池

1.3 JDK中自带的线程池

概述 : JDK对线程池也进行了相关的实现,在真实企业开发中我们也很少去自定义线程池,而是使用JDK中自带的线程池。

我们可以使用Executors中所提供的静态方法来创建不同类型的线程池:

代码实现:

newCachedThreadPool:

java 复制代码
package com.itheima.a01threadpool1;

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
    }
}
java 复制代码
package com.itheima.a01threadpool1;


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyThreadPoolDemo {
    public static void main(String[] args) throws InterruptedException {
    /*
        public static ExecutorService newCachedThreadPool()   :创建一个没有上限的线程池
        public static ExecutorService newFixedThreadPool (int nThreads) 创建有上限的线程池
    */


        //1.获取线程池对象
        ExecutorService pool1 = Executors.newCachedThreadPool();


        //2.给线程池提交任务
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());




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


    }
}

运行结果中,---前面就是线程默认的名字

newFixedThreadPool:

java 复制代码
package com.itheima.a01threadpool1;

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
    }
}
java 复制代码
package com.itheima.a01threadpool1;


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyThreadPoolDemo {
    public static void main(String[] args) throws InterruptedException {
    /*
        public static ExecutorService newCachedThreadPool()             创建一个没有上限的线程池
        public static ExecutorService newFixedThreadPool (int nThreads) 创建有上限的线程池
    */


        //1.获取线程池对象
        ExecutorService pool1 = Executors.newFixedThreadPool(3);


        //2.给线程池提交任务
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());




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


    }
}

1.4 自定义线程池

在自定义线程池中,当不断的提交任务时,有下面三个临界点:

  • 当核心线程满时,再提交任务,任务就会在任务队列中排队
  • 当核心线程满,任务队列也满时,才会创建临时线程
  • 当核心线程满,任务队列也满,临界线程也满时,最后就会触发任务拒绝策略

创建自定义线程池对象的语法:

使用ThreadPoolExecutor类的构造方法来创建自定义线程池对象:

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);

java 复制代码
package com.itheima.a02threadpool2;


import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MyThreadPoolDemo1 {
    public static void main(String[] args){

    /*
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor
        (核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);

        参数一:核心线程数量              不能小于0
        参数二:最大线程数                不能小于0,最大数量 >= 核心线程数量
        参数三:空闲线程最大的存活时间       不能小于0
        参数四:空闲线程最大存活时间的时间单位                  用TimeUnit枚举类型指定
        参数五:任务队列                  不能为null
        参数六:创建线程工厂              不能为null
        参数七:任务的拒绝策略             不能为null
    */
    ThreadPoolExecutor pool = new ThreadPoolExecutor(
            3,   //核心线程数,不能小于0
            6,   //最大线程数,不能小于0
            60,  //空闲线程最大存活时间
            TimeUnit.SECONDS,  //空闲线程最大存活时间的时间单位
            new ArrayBlockingQueue<>(3),   //任务队列
            Executors.defaultThreadFactory(),   //创建线程工厂
            new ThreadPoolExecutor.AbortPolicy()        //任务的拒绝策略是ThreadPoolExecutor类的内部类
    );




    }
}

1.6 创建自定义线程池时的参数详解

1.7 线程池-非默认任务拒绝策略

RejectedExecutionHandler是jdk提供的一个任务拒绝策略接口,它下面存在4个子类。

java 复制代码
ThreadPoolExecutor.AbortPolicy: 		    丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
ThreadPoolExecutor.DiscardPolicy: 		   丢弃任务,但是不抛出异常 这是不推荐的做法。
ThreadPoolExecutor.DiscardOldestPolicy:    抛弃队列中等待最久的任务 然后把当前任务加入队列中。
ThreadPoolExecutor.CallerRunsPolicy:        调用任务的run()方法绕过线程池直接执行。

注:明确线程池最多可执行的任务数 = 队列容量 + 最大线程数

1.8 线程池多大合适

最大并行数的概念,它和cpu的型号有关,用4核8线程的cpu去解释:

4核就好比cpu有四个大脑,能同时并行的做四件事情,然后利用超线程技术将原本的四个大脑虚拟成8个,就表示针对四核八线程的cpu而言,最大的并行数就为8

看项目的类型确定公式,去计算线程池的大小:

  • CPU密集型运算:如果项目中计算多,但读取本地文件或读取数据库的操作少,项目就属于这种,然后公式使用最大并行数+1即可
  • I/O密集型运算:如果项目读取本地文件或读取数据库的操作比较多,项目就属于这种,然后公式就使用最大并行数 * 期望CPU的利用率 * 总时间(:CPU的计算时间+等待时间)/CPU的计算时间
    • ​​​​​​​4核八线程:8 * 100% * 100%/50% = 16
相关推荐
kiiila4 分钟前
【Qt】控件概述和QWidget核心属性1(enabled、geometry、windowTitle、windowIcon、QRC机制)
开发语言·qt
ByteBlossom6666 分钟前
Go语言的数据库交互
开发语言·后端·golang
fadtes11 分钟前
C++ extern(八股总结)
开发语言·c++·算法
sukalot12 分钟前
windows C#-事件
开发语言·c#
yutian060613 分钟前
C# 去除字符串中首尾、首、尾、的空格
开发语言·c#
LoserChaser17 分钟前
windeployqt.exe打包qt程序总结(MSVC)
开发语言·qt
fadtes19 分钟前
C++ this指针(八股总结)
开发语言·c++
Trouvaille ~22 分钟前
CSDN 博客:CC++ 内存管理详解
java·c语言·c++
xweiran22 分钟前
Spring源码分析之事件机制——观察者模式(一)
java·开发语言·spring·观察者模式·事件机制
续亮~27 分钟前
Kafka核心参数与使用02
java·分布式·后端·kafka