十六、多线程(基础)(完结)

十六、多线程(基础)

16.1 线程相关概念

16.1.1 程序

  • 是为完成特定任务、用某种语言编写的一组指令的集合。
  • 简单的说:就是我们写的代码

16.1.2 进程

  • 进程是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存空间。当我们使用迅雷,又启动了一个进程,操作系统将为迅雷分配新的内存空间。
  • 进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程:有它自身的产生、存在和消亡的过程

16.1.3 线程

线程概念:

  • 线程由进程创建的,是进程的一个实体
  • 一个进程可以拥有多个线程,如图

单线程:同一个时刻,只允许执行一个线程

多线程:同一个时刻,可以执行多个线程,比如:一个qq进程,可以同时打开多个聊天窗口,一个迅雷进程,可以同时下载多个文件

16.1.4 并发和并行

并发:同一个时刻,多个任务交替执行,造成一种”貌似同时”的错觉,简单的说,单核cpu实现的多任务就是并发。

十六、多线程(基础)(完结)

并行:同一个时刻,多个任务同时执行。多核cpu可以实现并行。并发和并行

十六、多线程(基础)(完结)

16.2 线程基本使用

16.2.1 创建线程的两种方式

  • 实现 Runable 接口,重写 run() 方法
  • 继承 Thread 类,重写 run() 方法

16.2.2 使用 Thread 创建线程案例

十六、多线程(基础)(完结)
十六、多线程(基础)(完结)
/**
 * @author: Carl Zhang
 * @create: 2021-12-14 10:36
 *
 * 1)请编写程序,开启一个线程,该线程每隔1秒。在控制台输出"喵喵,我是小猫咪"
 * 2)对上题改进:当输出80次喵喵,我是小猫咪,结束该线程
 * 3)使用JConsole 监控线程执行情况,并画出程序示意图!
 *
 *  类继承了Thread类就是一个线程
 */
public class Cat extends Thread {
    /**
     * 重写 run() 方法,执行线程
     */
    @Override
    public void run() {
        /*
         * 统计执行次数
         * */
        int times = 1;
        int endTimes = 80;
        while (times

16.2.3 IDEA 启动 JConsole

  1. 启用JConsole:

一开始 IDEATerminal 输入 jconsole 提示:

十六、多线程(基础)(完结)
  1. 分析问题:

博客园找到文章,按照文章给的方法配置了参数还是相同提示
又百度知道 jconsolejdk 自带的工具,有可能是上次重装 jdk,环境变量没配好?
javac 了一下,果然环境变量有问题 。查看环境变量,原来是版本号没改,

十六、多线程(基础)(完结)
十六、多线程(基础)(完结)

重新配置好环境变量, javac 有参数, jconsole 也能启动。
再到 IDEATerminal 输入 jconsole,还是提示 “不是内部命令”

分析:
查看 terminal 的路径:

十六、多线程(基础)(完结)
输入 javac 提示:
十六、多线程(基础)(完结)
推测还是环境变量问题,经过百度,原来 CLASSPATH 没配置
重新配好 CLASSPATH=.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;

再到 IDEATerminal 输入 jconsole,还是提示 “不是内部命令”

果断放弃,用 cmd 输入 jconsole 启动

16.2.4 调用 star() 方法的原因

  • thread.run() 调用的是 run() 方法,会阻塞 main 方法
  • thread.start() 最终调用 start0() ,会创建线程,不会阻塞 main 方法

十六、多线程(基础)(完结)
package com.hspedu.thread;

/**
 * @author Carl Zhang
 * @description
 * @date 2021/12/14 20:21
 */
public class Thread01 {
    public static void main(String[] args) {
        //提示:不要显式创建线程,请使用线程池。
        Thread thread = new Thread();
        /*
        * 查看源码:
        * public synchronized void start() {
        *     ....

        *     //底层调用的是start0();
        *     //该方法是本地方法 private native void start0(); 由JVM来执行,底层是 c/c++实现
        *     //所以真正调用的是start0() 来实现多线程效果,而不是start()
        *     start0();
        * }
        * */

        //start() 会启动run()方法,具体细节看源码
        thread.start();
        //thread.run();
        /*
        * 总结:
        * 1. thread.run() 调用的是run() 方法,会阻塞main方法
        * 2. thread.start() 最终调用 start0() ,会创建线程,不会阻塞main方法
        *
        * */
    }
}

16.2.5 使用 Runnable 创建线程案例

原因:

  • java 是单继承的,有些类继承了别的类,但是还想创建线程,这时无法通过再继承 Thread 来创建线程
  • 这时可以用类实现 Runnable 接口来创建线程

案例:

  • 使用实现 Runnable 接口的方式,实现每隔1秒,打印一句话打印十次就退出
  • 底层其实是设计模式 – *代理模式
@SuppressWarnings("ALL")
public class Runnable01 {
    public static void main(String[] args) {
        //创建Work对象,来创建线程
        Work work = new Work();

        //这样是调用run() 方法,不是创建线程 线程名:main
        //work.run();

        /*
         通过往Tread的构造器传入实现了Runnable接口的Work对象,创建Tread对象
         通过Tread对象调用start()方法实现多线程
         这里其实是使用了代理模式,如何理解:Work类没有start方法,就通过创建Tread对象
         ,传入Work的对象,通过tread调用start() 最终执行work.start()效果
         提示:不要显式创建线程,请使用线程池。
        */
        Thread thread = new Thread(work);
        //线程名:Thread-0
        thread.start();
    }

    @Test
    public void ThreadProxyTest() {
        //创建ThreaProxy对象 传入work对象 最后执行work的run()
        ThreadProxy threadProxy = new ThreadProxy(new Work());
        threadProxy.start();
    }
}

package com.hspedu.threaduse;

/**
 * @author Carl Zhang
 * @description 模拟代理模式,可以把 ThreadProxy 当成一个 Thread
 * @date 2021/12/14 21:45
 */
public class ThreadProxy implements Runnable {

    private Runnable target;

    @Override
    public void run() {
        //如果target被赋值,就通过动态绑定调用target的run()
        if (target != null) {
            target.run();
        }
    }

    public ThreadProxy(Runnable target) {
        this.target = target;
    }

    public void start() {
        //start0() 才是真正使线程变成可运行状态 模拟的Thread
        start0();
    }

    public void start0() {
        run();
    }
}

package com.hspedu.threaduse;

/**
 * @author Carl Zhang
 * @description 通过实现Runnable接口实现多线程
 * @date 2021/12/14 21:29
 */
public class Work implements Runnable{

    @Override
    public void run() {
        int times = 1;
        int endTime = 8;

        //每次打印次数和线程名,打印完休眠1s
        while (times

16.2.6 使用 Callable 创建线程案例

案例:

  • 打印1 – 30,和线程名,每次间隔1s
  • 底层也是使用 *代理模式
import java.util.concurrent.Callable;

/**
 * @author: Carl Zhang
 * @create: 2021-12-27 16:08
 * 创建线程的第三种方法 - 实现 Callable 接口
 */
public class Thread05 implements Callable {

    @Override
    public Boolean call() throws Exception {
        //打印1 - 30,和线程名,每次间隔1s
        int endNum = 10;
        for (int i = 1; i  callable) {
        * 2. FutureTask类实现了RunnableFuture接口,RunnableFuture接口实现了Runnable接口
        *    所以FutureTask实现了Runnable接口
        * 3. 通过代理模式,将FutureTask对象作为参数创建Thread对象,从而调用start方法
        * 4. booleanFutureTask.get() 能获取线程返回结果,有阻塞作用
        * */

        Thread05 thread05 = new Thread05();

        FutureTask booleanFutureTask = new FutureTask<>(thread05);

        Thread thread = new Thread(booleanFutureTask);

        // 有阻塞作用
        Boolean aBoolean = booleanFutureTask.get();

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

        //线程结束时获取执行结果
        //Boolean aBoolean = booleanFutureTask.get();
        System.out.println(aBoolean);
    }
}

16.2.7 三种线程实现方式的区别

十六、多线程(基础)(完结)

16.2.8 Thread类中常用方法

  • String getName():返回此线程的名称
  • Thread 类中设置线程的名字
  • void setName(String name):将此线程的名称更改为等于参数 name
  • 通过构造方法也可以设置线程名称
  • public static Thread currentThread():返回对当前正在执行的线程对象的引用
  • public static void sleep(long time):让线程休眠指定的时间,单位为毫秒
/**
 * @author: Carl Zhang
 * @create: 2021-12-28 09:19
 *  Thread 类常用方法演示
 */
public class ThreadMethod {
    public static void main(String[] args) {
        //获取主线程名称
        Thread thread = Thread.currentThread();
        thread.setName("主线程");
        System.out.println("主线程名称:" + thread.getName());

        //- String getName():返回此线程的名称
        TimeThread timeThread = new TimeThread();
        System.out.println("线程名称:" + timeThread.getName());

        //- Thread类中设置线程的名字
        //
        //  - void setName(String name):将此线程的名称更改为等于参数 name
        timeThread.setName("时间线程");
        timeThread.start();

        //  - 通过构造方法也可以设置线程名称
        TimeThread timeThread2 = new TimeThread("时间线程2");
        timeThread2.start();
    }
}

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @author: Carl Zhang
 * @create: 2021-12-28 09:23
 * 隔1s打印一次当前时间
 */
public class TimeThread extends Thread{
    public TimeThread(String s) {
        super(s);
    }

    public TimeThread() {
    }

    @Override
    public void run() {
        while (true) {
            //获取当前日期对象,并格式化
            LocalDateTime now = LocalDateTime.now();
            DateTimeFormatter dateTimeFormatter =
                    DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
            String format = dateTimeFormatter.format(now);

            //- public static Thread currentThread():返回对当前正在执行的线程对象的引用
            System.out.println(format + ", 线程名:" + Thread.currentThread().getName());

            //- public static void sleep(long time):让线程休眠指定的时间,单位为毫秒
            //间隔1s
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  • public final void join() throws InterruptedException :让线程阻塞,执行完该线程才能执行其他线程
/**
 * @author: Carl Zhang
 * @create: 2021-12-28 09:53
 * 演示join方法的使用
 */
public class JoinThread {
    public static void main(String[] args) throws InterruptedException {
        int endTime = 100;
        // 通过匿名内部类创建线程并设置名称
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i

16.2.9 线程的优先级

  • 多线程的并发运行
  • 计算机中的 CPU,在任意时刻只能执行一条机器指令。每个线程只有获得 CPU 的使用权才能执行代码。
  • 各个线程 轮流获得 CPU 的使用权,分别执行各自的任务。
  • 线程有两种调度模型
  • 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
  • 抢占式调度模型( java 使用的):优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会 随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
  • 注意:优先级高不一定是先抢到执行权,只是几率更高而已,比如抽奖。
  • 设置和获取优先级案例
  • public final void setPriority(intnewPriority) 设置线程的优先级
  • public final intgetPriority() 获取线程的优先级
/**
 * @author: Carl Zhang
 * @create: 2021-12-28 10:17
 * 设置和获取线程的优先级
 */
@SuppressWarnings("ALL")
public class ThreadPriority {
    public static void main(String[] args) {
        int endTime = 100;

        //创建一个线程1,打印1 - 100 和线程名
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i  MAX_PRIORITY || newPriority < MIN_PRIORITY) {
              throw new IllegalArgumentException();
          }
        * */
        thread1.setPriority(1);

        //创建一个线程2,打印1 - 100 和线程名
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i

16.3 线程安全

16.3.1 线程安全问题引入 – 卖票案例

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

package com.hspedu.thread_safety;

import org.junit.jupiter.api.Test;

/**
 * @author: Carl Zhang
 * @create: 2021-12-28 13:43
 * 某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
 * 思路:
 * 票数量通用的 - 成员变量
 * 3个窗口同时卖 - 3个线程
 * 1. 用 Runnable 方式,子类 Ticket 实现 Runnable 接口
 * 2. 定义变量 ticketNum 保存票数
 * 3. 重写 run 方法循环卖票 - while(true)
 * 4. 退出条件 当票数 = 0 就退出循环
 * 5. 否则就出票,每次出票需要停顿
 * 6. 执行卖票操作:票数--,打印窗口名和票数
 * 7. 创建一个Ticket对象,创建三个Thread对象,将Ticket对象作为参数,并分别命名
 * 8. 分别开启三个线程,查看运行情况
 */
public class Ticket implements Runnable {
    /**
     * 2. 定义变量 ticketNum 保存票数
     */
    private int ticketNum = 100;

    /**
     * 3. 重写 run 方法循环卖票 - while(true)
     */
    @Override
    public void run() {
        while (true) {
            //4. 退出条件 当票数 = 0 就退出循环
            if (ticketNum == 0) {
                break;
            }
            //5. 否则就出票,每次出票需要停顿
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //6. 执行卖票操作:票数--,打印窗口名和票数
            ticketNum--;
            System.out.println(Thread.currentThread().getName() + ", 还剩 " +
                    ticketNum + "张票");
        }
    }
}

package com.hspedu.thread_safety;

import static org.junit.jupiter.api.Assertions.*;

/**
 * 7. 创建一个Ticket对象,创建三个Thread对象,将Ticket对象作为参数,并分别命名
 * 8. 分别开启三个线程,查看运行情况
 * */
class TicketTest {
    public static void main(String[] args) {
        //创建Ticket对象
        Ticket ticket = new Ticket();

        //创建三个线程
        Thread thread1 = new Thread(ticket, "窗口1");
        Thread thread2 = new Thread(ticket, "窗口2");
        Thread thread3 = new Thread(ticket, "窗口3");

        //开启线程
        thread1.start();
        thread2.start();
        thread3.start();

        //出现问题:
        // 1. 不同窗口的票数重复
        // 2. 结束时票数有误
    }
}

16.3.2 卖票案例问题分析

上述代码中出现了两个问题:

  • 不同窗口的票数重复

十六、多线程(基础)(完结)
  • 有负数票问题

十六、多线程(基础)(完结)

问题原因:多个线程在对共享数据进行读改写的时候,可能导致的数据错乱就是 线程的安全问题
解决方案:

  • 把多条线程操作共享数据的代码给 起来,让一个线程执行完,另一个线程才能执行该代码块
  • Java 提供了 同步代码块的方式来解决,引入同步代码块

16.3.3 线程的同步介绍

线程同步的概念:
java允许多线程并发执行,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突, 因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证该变量的唯一性和准确性。

分类:

  • 同步代码块。
  • 同步方法。
  • 锁机制。Lock

16.3.4 线程同步方式1 – 同步代码块

语法:

synchronized(任意对象) {
    //多条语句操作共享数据的代码
}

要点:

  • 默认情况锁是打开的,只要有一个线程进去执行代码了,锁就会关闭
  • 当线程执行完出来了,锁才会自动打开
  • 锁对象可以是任意对象 , 但是多个线程必须使用同一把锁
package com.hspedu.thread_lock;

/**
 * @author: Carl Zhang
 * @create: 2021-12-28 15:26
 * 用同步代码块优化卖票数据异常问题
 */
public class LockTicket implements Runnable {
    /**
     * 2. 定义变量 ticketNum 保存票数
     */
    private int ticketNum = 100;

    /**
     * 3. 重写 run 方法循环卖票 - while(true)
     */
    @Override
    public void run() {
        while (true) {
            /*
             * 结论:
             * 1. 使用 synchronized 代码块以避免在该线程没有完成操作之前,被其他线程的调用,从而保证该变量的唯一性和准确性。
             * 2. 括号中的锁 可以是任意对象
             * 3. 多个线程共用同一个锁对象
             * */

            //用同步代码块锁将对数据的操作锁起来
            synchronized ("lock") {
                //4. 退出条件 当票数 = 0 就退出循环
                if (ticketNum

同步代码块的优缺点:

  • 好处 : 解决了多线程的数据安全问题
  • 弊端 : 当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

16.3.5 线程同步方式2 – 同步方法

语法:

修饰符 synchronized 返回值类型 方法名(方法参数) {
    //多条语句操作共享数据的代码
}

要点:

  • 锁对象:同步方法不能指定锁对象的 , 但是有默认存在的锁对象的。
  • 非静态方法的锁对象是 this
  • 静态方法的锁对象是当前方法所在类的字节码对象(&#x7C7B;&#x540D;.class ),即 Ticket.class
package com.hspedu.threadsafe.synchronizemethod;

/**
 * @author Carl Zhang
 * @description
 * @date 2021/12/28 21:10
 * 通过线程安全方法优化卖票案例
 */
@SuppressWarnings("ALL")
public class Ticket implements Runnable {
    /**
     * 2. 定义变量 ticketNum 保存票数
     */
    private static int ticketNum = 100;

    /**
     * 3. 重写 run 方法循环卖票 - while(true)
     */
    @Override
    public void run() {
        while (true) {
            if (sellTicket()) {
                break;
            }
        }
    }

    /**
     * 用同步方法将卖票的具体操作锁起来
     *
     * 结论:
     * 锁对象:同步方法不能指定锁对象的 , 但是有默认存在的锁对象的。
     *  1. 非静态方法的锁对象是this
     *  2. 静态方法的锁对象是当前方法所在类的字节码对象(类名.class),即Ticket.class
     */
    //private synchronized boolean sellTicket() {
    private static synchronized boolean sellTicket() {
        //4. 退出条件 当票数

同步代码块和同步方法的区别:

  • 同步代码块可以锁住指定代码,同步方法是锁住方法中所有代码
  • 同步代码块可以指定锁对象,同步方法不能指定锁对象

16.3.6 线程同步方式3 – Lock锁

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

使用:

  • 方法:Lock中提供了获得锁和释放锁的方法
  • void lock():获得锁
  • void unlock():释放锁
  • 实例化: Lock 是接口不能直接实例化,这里采用它的实现类 ReentrantLock 来实例化
  • Lock _lock _= new ReentrantLock();: 通过 ReentrantLock 的构造方法实例化 lock
package com.hspedu.lock;

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

/**
 * @author: Carl Zhang
 * @create: 2021-12-29 09:24
 * 使用Lock锁优化Ticket线程安全问题
 */
public class Ticket implements Runnable {
    private int ticketNum = 100;

    /**
     * 通过实现类ReentrantLock创建Lock实例化
     * 用static 修饰,使所有对象共用一个锁
     */
    static final Lock LOCK = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            //获得锁
            //注意:lock()方法要在try代码块外,且方法与try代码块间不能有方法抛异常,否则会加锁失败
            LOCK.lock();
            try {
                if (ticketNum == 0) {
                    break;
                }
                ticketNum--;
                System.out.println(Thread.currentThread().getName() + ", 还剩 " +
                        ticketNum + "张票");
                Thread.sleep(100);
            } catch (Exception e) { //将sleep的异常处理放到外面,简化代码
                e.printStackTrace();
            } finally {
                //释放锁
                //注意:释放锁必须在finally代码块中,才能确保每次都执行
                LOCK.unlock();
            }
        }
    }
}

package com.hspedu.lock;

/**
 * 7. 创建一个Ticket对象,创建三个Thread对象,将Ticket对象作为参数,并分别命名
 * 8. 分别开启三个线程,查看运行情况
 * */
class TicketTest {
    public static void main(String[] args) {
        //创建Ticket对象
        Ticket ticket1 = new Ticket();
        Ticket ticket2 = new Ticket();
        Ticket ticket3 = new Ticket();

        //创建三个线程
        Thread thread1 = new Thread(ticket1, "窗口1");
        Thread thread2 = new Thread(ticket2, "窗口2");
        Thread thread3 = new Thread(ticket3, "窗口3");

        //开启线程
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

注意:

  • lock() 方法要在 try 代码块外,且方法与 try 代码块间不能有方法抛异常,否则会加锁失败
  • 释放锁必须在 finally 代码块中,才能确保每次都执行
  • 多个线程使用相同的 Lock 锁对象,需要多线程操作数据的代码放在 lock()unLock() 方法之间。一定要确保 unlock() 最后能够调用

16.3.7 解决单例模式的线程安全问题

  • 问题
  • 懒汉单例设计模式在多线程环境下可能会实例化出多个对象,不能保证单例的状态,所以加上关键字: synchronized ,保证其同步安全。
  • 注:有关单例模式的介绍见10.4 单例设计模式

  • 需求:使用懒汉单例 ,定义一个皇帝类,要求对象只能存在一个
package com.itheima.singledesign;

/*
    需求 : 使用单例模式(懒汉式) , 要求此类只能有一个对象
 */
public class King2 {
    //  1. 将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。
    private King2() {
    }

    // 2. 在该类内部定义一个private static修饰的成员变量 . 此变量不需要赋值
    private static King2 king;

    // 3. 定义一个静态方法返回这个唯一对象。 此方法需要加上synchronized关键字保证在多线程中也只有一个实例对象
    public static synchronized King2 getInstance() {
        if (king == null) {
            king = new King2();
        }
        return king;
    }
}
package com.itheima.singledesign;

import org.junit.Test;

public class KingTest {
    @Test
    public void show2() {
        // 测试懒汉式单例模式
        for (int i = 0; i < 10; i++) {
            King2 k = King2.getInstance();
            System.out.println(k);
        }
    }
}

16.4 死锁

16.4.1 死锁的介绍

概念:两个线程对两个同步锁对象具有循环依赖时,就会大概率的出现死锁。我们要避免死锁的产生。否则一旦死锁,除了重启没有其他办法的

16.4.2 死锁的产生及避免

死锁产生条件:

  • 多个线程
  • 多个线程的锁有相互依赖
  • 同步代码块的锁进行嵌套使用 , 就会大概率产生死锁
package com.hspedu.deadlock;

/**
 * @author: Carl Zhang
 * @create: 2021-12-29 10:07
 * 演示死锁的产生
 * 死锁:两个线程对两个同步锁对象具有循环依赖时,就会大概率的出现死锁。
 */
@SuppressWarnings("ALL")
public class DeadLock {
    public static void main(String[] args) {
        //2. 创建两个同步锁对象
        String left = "筷子左";
        String right = "筷子右";

        /*
        * 问题:程序卡死,出现了死锁
        *       马大帅拿到了筷子左等待筷子右
        *       范德彪拿到了筷子右等待筷子左
        * 分析:
        *       马大帅线程的left锁关闭,等待right锁,此时cpu执行权被范德彪抢到
        *       范德彪线程的right锁关闭,等待left锁,而left锁此时是关闭的,出现死锁
        *
        * 结论:
        * 死锁产生条件:
        * 1. 多个线程
        * 2. 多个线程的锁有相互依赖
        * 即:同步代码块的锁进行嵌套使用 , 就会大概率产生死锁
        *
        * 解决:
        * 避免使用同步代码块嵌套
        * */

        //1. 创建两个线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                //3. 使两个线程里使用两个同步锁产生循环依赖
                //   - 两个线程里的两个同步代码块循环使用不同的锁嵌套
                while (true){
                    synchronized (left) {
                        System.out.println(Thread.currentThread().getName() +
                                "拿到了" + left + "等待" + right);
                        synchronized (right) {
                            System.out.println(Thread.currentThread().getName() +
                                    "拿到了" + left + "和" + right + ", 开吃");

                        } //释放right锁
                    } //释放left锁
                }
            }
        }, "马大帅").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                //3. 使两个线程里使用两个同步锁产生依赖
                //   - 两个线程里的两个同步代码块循环使用不同的锁嵌套
                while (true) {
                    synchronized (right) {
                        System.out.println(Thread.currentThread().getName() +
                                "拿到了" + right + "等待" + left);
                        synchronized (left) {
                            System.out.println(Thread.currentThread().getName() +
                                    "拿到了" + right + "和" + left + ", 开吃");

                        } //释放left锁
                    } //释放right锁
                }
            }
        }, "范德彪").start();

    }
}

避免死锁:避免使用同步代码块嵌套

16.5 线程状态

16.5.1 线程的六种状态

十六、多线程(基础)(完结)

16.5.2 线程的六种状态产生条件

十六、多线程(基础)(完结)

16.6 线程通讯

16.6.1 线程通讯概念

线程间的通讯技术就是通过等待和唤醒机制,来实现多个线程协同操作完成某一项任务,例如经典的生产者和消费者案例

16.6.2 等待和唤醒机制介绍

等待唤醒机制其实就是让线程进入等待状态或者让线程从等待状态中唤醒,需要用到两种方法,如下:

  • 等待方法
  • void wait() 让线程进入无限等待。
  • void wait(long timeout) 让线程进入计时等待
  • 以上两个方法调用会导致当前线程释放掉锁资源。
  • 唤醒方法
  • void notify() 唤醒在此对象监视器(锁对象)上等待的单个线程。
  • void notifyAll() 唤醒在此对象监视器上等待的所有线程。
  • 以上两个方法调用不会导致当前线程释放掉锁资源。

注意事项

  • 等待的方法会释放锁,唤醒的方法不会释放锁
  • 等待和唤醒的方法,都要使用锁对象调用(需要在同步代码块中使用,因为同步代码块能指定锁对象)。
  • 等待和唤醒方法应该使用相同的锁对象调用。

16.6.3 等待唤醒案例1 – 无限等待

package com.hspedu.threadwait;

/**
 * @author: Carl Zhang
 * @create: 2021-12-29 14:18
 * 无限等待案例
 * 注意:进入无限等待需要使用锁在同步代码中调用wait方法。
 */
@SuppressWarnings("ALL")
public class InfiniteWait {
    public static void main(String[] args) {
        //创建锁对象
        String lock = "锁";

        //创建一个线程
        //线程里面传入Runnable匿名内部类,重写run方法
        new Thread(new Runnable() {
            @Override
            public void run() {
                //使用同步代码块操作锁对象
                synchronized (lock) {
                    //等待前后在控制台打印提示
                    System.out.println("进入" + Thread.currentThread().getName()
                            + "线程");
                    System.out.println("线程进入无限等待....");
                    try {
                        lock.wait(); //进入无限等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程被唤醒.....");
                }
            }
        }, "跑步").start();
    }
}

16.6.4 等待唤醒案例2 – 无限等待被唤醒

package com.hspedu.threadwait;

/**
 * @author: Carl Zhang
 * @create: 2021-12-29 14:26
 * 线程进入无限等待后被唤醒
 * 注意:等待和唤醒是两个或多个线程之间实现的。进入无限等待的线程是不会自动唤醒,只能通过其他线程来唤醒。
 */
@SuppressWarnings("ALL")
public class WaitNotify {
    public static void main(String[] args) {
        //创建一个锁对象
        String lock = "锁";

        //创建一个线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                String name1 = Thread.currentThread().getName();

                // 1. 使用同步代码块操作锁对象
                synchronized (lock) {
                    System.out.println("进入" + name1 + "线程");
                    System.out.println("线程进入无限等待.....");
                    try {
                        // 2. 通过锁对象调用等待方法
                        lock.wait(); //释放lock锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    lock.notify(); //唤醒其他线程
                    //lock.notify(); //被无限等待的线程无法自主唤醒,需要其他线程来唤醒
                    System.out.println(name1 + "线程被唤醒.....");
                }
            }
        }, "跑步").start();

        //创建另一个线程唤醒 跑步 线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                String name2 = Thread.currentThread().getName();

                //等待1000,确保第一个线程先执行
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //使用同步代码块操作锁对象
                synchronized (lock) {
                    System.out.println("进入" + name2 + "线程");
                    System.out.println(name2 + "线程随机唤醒一个无限等待的线程");
                    lock.notify(); //唤醒一个随机线程,不释放锁
                    try {
                        Thread.sleep(5000); //如果notify()释放了锁,此时线程1应该被唤醒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "休息").start();

        /*
         * 结论:
         * 1. 被无限等待的线程无法自主唤醒,需要其他线程来唤醒
         * 2. lock.notify(); 唤醒一个随机线程,不释放锁
         * */
    }
}

16.6.5 等待唤醒案例3 – 计时等待

package com.hspedu.threadwait;

/**
 * @author: Carl Zhang
 * @create: 2021-12-29 15:16
 * 线程进入计时等待并唤醒
 * 注意:进入计时等待的线程,时间结束前可以被其他线程唤醒。时间结束后会自动唤醒
 */
@SuppressWarnings("ALL")
public class TimeWait {
    public static void main(String[] args) {
        // 创建一个锁对象
        String lock = "锁";
        // 使用Runnable作为参数创建一个跑步线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                String name1 = Thread.currentThread().getName();
                //   使用同步代码块操作锁对象
                synchronized (lock) {
                    System.out.println("进入" + name1 + "线程");
                    System.out.println("线程进入5s计时等待....");
                    long time1 = System.currentTimeMillis();
                    try {
                        // 通过wait方法使跑步线程进入计时等待5s
                        lock.wait(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //   获取线程等待的时间 - 通过前后的时间相减
                    long time2 = System.currentTimeMillis();
                    System.out.println(name1 + "线程被唤醒,等待了" +
                            (time2 - time1) + "ms");
                }
            }
        }, "跑步").start();

        // 使用Runnable作为参数创建一个休息线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 使用同步代码块操作锁对象
                synchronized (lock) {
                    System.out.println("进入" +
                            Thread.currentThread().getName() + "线程");
                    System.out.println("随机唤醒一个等待中的线程...");
                    //   使用notify方法唤醒跑步线程
                    lock.notify();
                }
            }
        }, "休息").start();
    }
}

16.6.6 生产者消费者案例

需求:厨师把做好的汉堡放到桌子上,吃货来吃。如果厨师没做好,吃货就要等,如果吃货没吃完,厨师也要等。且一天的汉堡数量是10个。通过多线程实现

十六、多线程(基础)(完结)

分析:

  1. 有吃货(生产者)线程,厨师(消费者)线程,桌子三个对象
  2. 消费者的操作:
  3. 先判断桌子上有没有汉堡
  4. 如果有,就拿走桌子上的汉堡,花费2s吃掉
  5. 如果吃完了,就唤醒厨师继续做汉堡
  6. 如果没有,就等待
  7. 如果吃完10个,就结束
  8. 桌子:
  9. 通过桌子上有没有汉堡控制生产者和消费者两个线程
  10. 保存每日定量的汉堡
  11. 生产者操作:
  12. 先判断桌子上有没有汉堡
  13. 如果有,就等待消费者吃完
  14. 如果没有,就做汉堡,耗费2s,做好了把汉堡放在桌子上,并唤醒消费者开吃。
  15. 如果今天做完10个汉堡了,就结束
package com.hspedu.hamburger;

/**
 * @author: Carl Zhang
 * @create: 2021-12-29 16:15
 * 桌子:
 * 1. 通过桌子上有没有汉堡控制生产者和消费者两个线程 -- boolean 变量
 * 2. 保存唯一的锁
 * 3. 保存每日定量的汉堡
 */
public class Table {
    //static int hamburgerNum = 0;
    /**
     * 通过桌子上有没有汉堡控制生产者和消费者两个线程 -- boolean 变量
     * 默认是false 没有汉堡
     */
    static boolean hamburgerNum = false;

    /**
     * 每日汉堡数,默认是10个
     * */
    static int allHamburgerNum = 10;

    private static final Table TABLE = new Table();

    private Table() {
    }

    /**
     * 使用单例,让Table对象只有一个
     * 使两个线程拿到同一个锁
     */
    public static Table getTable() {
        return TABLE;
    }
}
package com.hspedu.hamburger;

/**
 * @author: Carl Zhang
 * @create: 2021-12-29 16:13
 * 生产者操作:
 * 1. 先判断桌子上有没有汉堡
 * 2. 如果有,就等待消费者吃完
 * 3. 如果没有,就做汉堡,花费2s,做好了把汉堡放在桌子上,并唤醒消费者开吃
 * 4. 如果今天做完10个汉堡了,就结束
 */
public class Producer implements Runnable {
    @Override
    public void run() {

/*
        问题:
         1.问什么不用for而用while
           如果是for循环,一开始没有汉堡也会多走一次循环
           今天没汉堡 -> 进入循环 ->  厨师制作,吃货等待 -> 没有循环次数,退出

         2.为什么不用 while (Table.allHamburgerNum  0) {
            //桌子上的汉堡不能有线程安全问题,所以要对桌子进行上锁
            //两个类的锁要是同一个对象
            synchronized (Table.getTable()) {
                //如果放在最后,今天没汉堡也会进行制作
                //做完10个
                if (Table.allHamburgerNum
package com.hspedu.hamburger;

/**
 * @author: Carl Zhang
 * @create: 2021-12-29 16:15
 * 消费者的操作:
 * 1. 先判断桌子上有没有汉堡
 * 2. 如果有,就拿走桌子上的汉堡,花费2s吃掉
 * 3. 如果吃完了,就唤醒厨师继续做汉堡
 * 4. 如果没有,就等待
 * 5. 如果吃完10个,就结束
 */
public class Consumer implements Runnable {

    @Override
    public void run() {
        while (true) {
            //桌子上的食物数量不能发生线程安全问题,所以要对桌子上锁
            //要用到等待唤醒机制,所以通过同步代码块对锁进行操作
            synchronized (Table.getTable()) {
                //编程思想:break语句放到循环最上面,如果不满足条件就直接退出
                // 如果放在最后面,今日没汉堡也会进行等待
                // 吃完了10个
                if (Table.allHamburgerNum

16.7 线程池

16.7.1 手动创建多个线程的缺陷

当要使用多个线程来执行简单的任务时,手动创建线程的方式会使系统频繁创建和销毁线程,线程不可复用,且会消耗大量资源。

16.7.2 引入线程池

可以使用线程池的方式来解决

概念:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

16.7.3 线程池的好处

  • 可复用,降低资源消耗:减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  • 提高响应速度。 : 当任务到达时,任务可以不需要等待线程创建 , 就能立即执行。
  • 提高线程的可管理性。 : 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存 , 服务器死机 (每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

16.7.4 线程池使用步骤

  1. 创建线程池指定线程开启的数量
  2. 提交任务给线程池,线程池中的线程就会获取任务,进行处理任务。
  3. 线程处理完任务,不会销毁,而是返回到线程池中,等待下一个任务执行。
  4. 如果线程池中的所有线程都被占用,提交的任务,只能等待线程池中的线程处理完当前任务

16.8 线程池API介绍

16.8.1 线程池API介绍

线程池 java.util.concurrent.ExecutorService 是一个接口,不能实例化

java 提供了 工具类来获得线程池子类的实例化 java.util.concurrent.Executors

  • ExecutorService executorService = Executors.newFixedThreadPool(3); 3表示该线程池最多3个线程
  • 该方法内部返回了一个线程池实现类对象 return new ThreadPoolExecutor(...)

16.8.2 线程池相关方法

  • 提交任务:
  • Future<?> submit(Runnable task) 可传入 Runnable 对象
  • <t> Future<t> submit(Callable<t> task);</t></t></t> 可传入 Callable 对象
  • 关闭线程池:
  • void shutdown() 启动一次顺序关闭,执行以前提交的任务,但不接受新任务。
  • 注意:一般不使用关闭方法,除非后期不用或者很长时间都不用,就可以关闭

16.9 线程池处理Runnable任务

16.9.1 实现步骤

  1. 创建线程池指定线程个数
  2. 定义 Runnable 任务类,
  3. 提交任务给线程池

16.9.2 案例

package com.hspedu.threadpool;

import java.lang.reflect.Executable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author: Carl Zhang
 * @create: 2021-12-30 13:45
 * 使用线程池模拟游泳教练教学生游泳。
 * 游泳馆(线程池)内有3名教练(线程),游泳馆招收了5名学员学习游泳(任务)。
 */
@SuppressWarnings("ALL")
public class RunnableThreadPool {
    public static void main(String[] args) {
        /*
        * 结论:
        * 1. 线程池ExecutorService是一个接口,不能实例化
        * 2. java提供了工具类来获得线程池子类的实例化 Executors.newFixedThreadPool(3); 3是最多3个线程
        * 3. 一般不使用关闭方法,除非后期不用或者很长时间都不用,就可以关闭
        * */

        // 步骤:
        // 1. 创建线程池指定线程开启的数量
        // 通过工具类Executors获取线程池实例
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        // 2. 提交任务给线程池,线程池中的线程就会获取任务,进行处理任务。
        executorService.submit(new Student("小红"));
        executorService.submit(new Student("小黑"));
        executorService.submit(new Student("小蓝"));
        executorService.submit(new Student("小白"));
        executorService.submit(new Student("小绿"));

        // 3. 线程处理完任务,不会销毁,而是返回到线程池中,等待下一个任务执行。
        //手动销毁线程池
        executorService.shutdown();
    }
}

package com.hspedu.threadpool;

/**
 * @author: Carl Zhang
 * @create: 2021-12-30 13:47
 * 学员学习游泳(任务)。
 */
public class Student implements Runnable{
    private String name;

    public Student(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()
                + "教" + name + "学习游泳....");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(name + "学完了");
    }
}

16.10 线程池处理Callable任务

16.10.1 Callable接口执行任务的特点

十六、多线程(基础)(完结)
十六、多线程(基础)(完结)

Callable与Runnable的不同点:

  • Callable 支持结果返回, Runnable 不行
  • Callable 可以抛出异常, Runnable 不行

16.10.2 实现步骤

  1. 创建线程池
  2. 定义 Callable 任务
  3. 提交任务给线程池
  4. <t> Future<t> submit(Callable<t> task);</t></t></t> 可传入 Callable 对象,返回一个带任务执行结果的对象
  5. Future 接口里有一个方法 get() ,用来返回任务执行的结果
  6. 获取执行结果

16.10.3 案例

package com.hspedu.threadpool;

import java.util.concurrent.*;

/**
 * @author: Carl Zhang
 * @create: 2021-12-30 14:30
 * 使用线程池执行Callable任务
 * 需求:使用线程池计算 从0~n的和,并将结果返回
 *
 * 步骤:
 * 1. 创建线程池
 * 2. 定义Callable任务
 * 3. 创建Callable任务,提交任务给线程池
 * 4. 获取执行结果
 */
@SuppressWarnings("ALL")
public class CallableThread {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1. 创建线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(1);

        //2. 定义任务 - Callable类型,计算0~n的和 Sum类
        //3. 提交任务
        Future submit = threadPool.submit(new Sum(10));

        //4. 获取执行结果
        Integer sum = submit.get();
        System.out.println("线程执行结果:" + sum);

        //关闭线程池
        threadPool.shutdown();
    }
}

16.11 自定义线程池

16.11.1 常规线程池的弊端

问题: Executors.newFixedThreadPool(int nThreads); 方法返回的线程池实例 return new ThreadPoolExecutor(nThreads, ...) ,构造器中的参数是固定的,会遇到无法满足我们需求的时候

解决:此时可以通过 ThreadPoolExecutor 类的构造方法手动创建线程池

16.11.2 ThreadPoolExecutor类继承关系

十六、多线程(基础)(完结)

16.11.3 ThreadPoolExecutor类的构造方法解析

构造方法参数解析:

public ThreadPoolExecutor(
    int corePoolSize,     //核心线程数 :随着线程池销毁才销毁, 不能小于0
    int maximumPoolSize,  //最大线程数 :- 核心线程数 = 临时线程数,不能小于等于0,最大数量>=核心线程数量
    long keepAliveTime,   //临时线程存活时间, 不能小于0
    TimeUnit unit,        //临时线程存活时间单位,TimeUnit是枚举类型,保存了时间单位
    BlockingQueue workQueue, //阻塞队列:超过了队列容纳的数量就要等待,不能为null
    ThreadFactory threadFactory, //创造线程模式,不能为null
    RejectedExecutionHandler handler) //拒绝策略:超过了可执行的总任务数(最大线程数 + 阻塞队列数)就执行拒绝策略,不能为null
{ ... }

核心员工:不变的,没有任务也不辞退,只有饭店倒闭才辞退
临时员工:可变的,当临时员工有一段时间闲着了,为了节约成本,老板就需要把临时员工给辞掉

案例:

package com.hspedu.threadpool;

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

/**
 * @author Carl Zhang
 * @description
 * @date 2021/12/30 22:11
 * 自定义线程池:通过手动创建线程池接口的实现类对象来创建线程池
 */
public class CustomThreadPool {
    public static void main(String[] args) {
        //手动创建线程池接口的实现类对象来创建线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                //核心线程数 2
                2,
                //最大线程数4,其中2个临时线程数
                4,
                //线程存活的时间60,单位分钟
                60,
                TimeUnit.MINUTES,
                //阻塞队列,队列容纳的任务量是1,ArrayBlockingQueue是BlockingQueue接口的实现类
                new ArrayBlockingQueue<>(1),
                //使用默认的线程创造方式
                Executors.defaultThreadFactory(),
                // 使用默认的拒绝策略
                // ThreadPoolExecutor的内部类AbortPolicy是RejectedExecutionHandler接口的实现类,
                // 该拒绝策略表示超过线程池的最大任务量就丢弃多余的任务并抛异常
                new ThreadPoolExecutor.AbortPolicy()
        );

        //创建任务并提交
        threadPool.submit(new Sum(10));
        threadPool.submit(new Student("老大"));
        threadPool.submit(new Student("老二"));
        threadPool.submit(new Student("老三"));
        threadPool.submit(new Student("老四"));
        threadPool.submit(new Student("老五"));

        //此处任务量超过了该线程池的总任务量5个,执行拒绝策略:
        //      抛异常java.util.concurrent.RejectedExecutionException
        //      拒绝了此次任务,继续执行上述任务
        threadPool.submit(new Student("老六"));
    }
}

16.11.4 四种拒绝策略

概念:当提交给线程池的任务数量超过了总任务数(最大线程数 + 阻塞队列容量),就会执行拒绝策略

四种拒绝策略

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

16.12 练习

16.12.1 练习1 卖票问题

package com.hspedu.homework;

/**
 * @author: Carl Zhang
 * @create: 2021-12-31 09:43
 * 请使用"同步方法"编写程序,模拟三个窗口(三个线程)同时卖100张票(共同的操作对象)的情况
 */
public class Window03 implements Runnable {

    @Override
    public void run() {
        //while (true) {
        while (sell()) {
            //if (Ticket.num

16.12.2

Original: https://www.cnblogs.com/Carl-Zhang/p/15763060.html
Author: Carl-Zhang
Title: 十六、多线程(基础)(完结)

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/572492/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球