多线程详解(理论与实践的最佳融合方案)

首先声明版权问题,部分内容迁移学习来自于哔哩哔哩狂神说秦疆老师,此处致敬! 字数达两万之多,用时两夜,不容易!其实不想深入了解这块,但是,人总要走出舒适圈,所以,就认认真真学了一遍,这里与大家共勉! 一.概述 线程简介

在这里插入图片描述

首先声明版权问题,部分内容迁移学习来自于哔哩哔哩狂神说秦疆老师,此处致敬!
字数达两万之多,用时两夜,不容易!其实不想深入了解这块,但是,人总要走出舒适圈,所以,就认认真真学了一遍,这里与大家共勉!

一.概述

线程简介

总而言之,就是在同一时间,做了不同的事情,正所谓一石二鸟,一箭双雕,赔了夫人又折兵

生活中很多事情都可以看作是多线程的例子。比如:

  1. 煮饭:煮饭需要同时加热米饭和煮菜,如果只有一个火力,就只能按照一定的顺序依次进行,而使用多个火力则可以同时加热多个部分,从而节省时间。
  2. 洗衣服:洗衣服需要同时洗涤多个衣物,如果只有一个洗衣机,就只能按照一定的顺序依次进行,而使用多个洗衣机则可以同时洗涤多个衣物,从而节省时间。
  3. 开车:开车需要同时控制加速、刹车、方向盘等多个部分,如果只有一个人,就只能按照一定的顺序依次进行,而使用多个人则可以同时控制多个部分,从而提高驾驶的安全性和效率。

然后,我们使用官方点的语言解释一下什么是多线程:

多线程是一种计算机程序设计技术,它可以同时执行多个线程(或者说子任务),以实现更高效的计算机程序。在单线程的计算机程序中,程序只能按照一定的顺序执行,而在多线程的程序中,多个线程可以在同一时间内并发执行,从而提高程序的并发性和执行效率。

举个例子,如果你正在使用电脑进行网页浏览,当你点击某个链接时,网页就会从服务器端获取相应的内容并在你的电脑上显示出来。如果网页的内容非常多,而你的电脑只有一个处理器,那么你就只能等待这些内容全部加载完成后才能浏览网页。但如果你使用的是多线程的浏览器,就可以同时加载多个网页的内容,从而提高浏览网页的速度。

多线程技术在现代计算机系统中非常重要,广泛应用于各种计算机程序中,如Web服务器、操作系统、数据库等等。它可以帮助计算机程序更好地利用多核CPU,提高程序的响应速度和并发能力。

总的来说,多线程技术可以帮助我们更好地利用多核CPU,提高程序的响应速度和并发能力,从而提高我们的生活效率。

普通方法调用和多线程对比

image-20230523233120949

线程和进程的联系与区别

进程和线程都是操作系统中的基本概念,但它们有很不同的特点和用途。

相同点:

  1. 进程和线程都可以作为程序的执行单位;
  2. 一个进程可以包含多个线程;
  3. 进程和线程都具有并发性。

不同点:

  1. 进程是操作系统资源分配的基本单位,而线程是CPU调度的基本单位。进程拥有自己独立的地址空间、系统资源(如文件句柄、打开网络连接等)和内核对象(如进程句柄、信号处理函数等),一个进程无法访问另一个进程的资源,而线程则共享所属进程的资源,如打开的文件、上下文环境等;
  2. 创建或销毁进程时,操作系统需要完成大量的工作,比如分配和初始化各种数据结构、加载程序代码和数据等,这将花费较大的时间和系统开销。而创建或销毁线程时,操作系统只需实现简单的数据结构即可,工作量远远小于进程,因此更加轻量级;
  3. 进程之间不能直接通信,必须通过一些机制(如管道、消息队列、共享内存等)来进行通信,而线程间可以直接共享相同的内存空间,并通过读写同一变量等方式来进行通信,这种通信的效率更高;
  4. 进程启动的时间长,切换上下文所需的时间较多,线程则启动快、上下文切换快。

总结来说,进程和线程是操作系统中不同层级的并发调度单位,进程之间相互隔离,线程之间共享资源,根据具体应用场景选择合适的方案可提高程序性能。

线程实现

Java多线程有以下五种实现方法:

  1. 继承Thread类

继承Thread类是实现多线程最常用的方法。通过继承Thread类,可以重写run()方法,在run()方法中编写多线程的逻辑代码。如下所示:

public class MyThread extends Thread {
   public void run() {
       // 多线程的逻辑代码
   }
}
  1. 实现Runnable接口

实现Runnable接口也是实现多线程的一种方式。通过实现Runnable接口,可以创建多个实例对象,然后将它们作为参数传递给Thread类的构造方法中。如下所示:

public class MyRunnable implements Runnable {
   public void run() {
       // 多线程的逻辑代码
   }
}

public class MyThread extends Thread {
   private MyRunnable mRunnable;
   
   public void setRunnable(MyRunnable mRunnable) {
       this.mRunnable = mRunnable;
   }
   
   @Override
   public void run() {
       mRunnable.run();
   }
}
  1. 实现Callable接口

实现Callable接口与实现Runnable接口类似,不同之处在于Callable接口可以返回一个结果。Callable接口可以用来执行一些需要返回结果的任务。如下所示:

public class MyCallable implements Callable<Integer> {
   public Integer call() throws Exception {
       // 多线程的逻辑代码,返回一个整数结果
       return result;
   }
}

public class MyThread extends Thread {
   private MyCallable mCallable;
   
   public void setCallable(MyCallable mCallable) {
       this.mCallable = mCallable;
   }
   
   @Override
   public Integer call() throws Exception {
       return mCallable.call();
   }
}
  1. 实现Future接口

Future接口可以用来获取异步计算的结果。通过实现Future接口,可以在调用异步方法时获取到异步计算的结果。如下所示:

public class MyFuture implements Future<Integer> {
   private Integer result;
   
   public void setResult(Integer result) {
       this.result = result;
   }
   
   @Override
   public Integer get() throws Exception {
       // 在获取结果前可以进行一些操作,如等待一段时间等操作。这里直接返回计算结果。如果计算还没有完成,则会一直阻塞等待。如果计算已经完成,则会立即返回计算结果。如果出现异常,则会抛出异常。如下所示:
       while (!isDone()) {} 
       // 这里是一个无限循环,直到isDone()返回true才跳出循环。isDone()表示异步计算是否已经完成。如果isDone()返回true,则说明异步计算已经完成。否则,在下一次循环中继续等待。这里使用了一个while循环来等待计算结果的返回。这种方式适用于不需要知道异步计算何时完成的情况。如果需要知道异步计算何时完成,可以使用FutureTask类来进行异步计算并获取异步计算的结果。如下所示:
       

在这里,我们再稍微多讲一点

Java多线程五种实现方法可以混合使用。

在实际开发中,我们可以根据需要选择不同的多线程实现方式。例如,如果我们需要执行一个异步计算的任务,可以使用Callable接口和Future接口来实现;如果我们需要执行一个有返回值的任务,可以使用Runnable接口和Future接口来实现;如果我们需要执行一个线程池中的任务,可以使用Thread类或Runnable接口来实现。

同时,我们也可以将多种多线程实现方式进行组合使用。例如,我们可以将Runnable接口和Callable接口进行组合使用,来实现一个既可以执行任务又可以返回结果的多线程程序。如下所示:

public class MyRunnable implements Runnable, Callable<Integer> {
   private Integer result;
   
   public void run() {
       // 多线程的逻辑代码
       result = 123; // 这里可以设置异步计算的结果
   }
   
   @Override
   public Integer call() throws Exception {
       // 在获取结果前可以进行一些操作,如等待一段时间等操作。这里直接返回计算结果。如果计算还没有完成,则会一直阻塞等待。如果计算已经完成,则会立即返回计算结果。如果出现异常,则会抛出异常。如下所示:
       while (!isDone()) {} // 这里是一个无限循环,直到isDone()返回true才跳出循环。isDone()表示异步计算是否已经完成。如果isDone()返回true,则说明异步计算已经完成。否则,在下一次循环中继续等待。这里使用了一个while循环来等待计算结果的返回。这种方式适用于不需要知道异步计算何时完成的情况。如果需要知道异步计算何时完成,可以使用FutureTask类来进行异步计算并获取异步计算的结果。如下所示:
       while (!isDone()) {} // 这里是一个无限循环,直到isDone()返回true才跳出循环。isDone()表示异步计算是否已经完成。如果isDone()返回true,则说明异数

线程状态

常见的线程状态包括:

  1. 新建(New):当一个线程被创建但还没有开始运行时,它的状态为新建状态。
  2. 就绪(Runnable):当一个线程已经创建,且它的所有前置条件都已完成,它等待CPU时间片分配以执行任务时,它的状态为就绪状态。
  3. 运行(Running):当一个线程得到了处理器分配时间并正在执行任务时,它的状态为运行状态。
  4. 阻塞(Blocked):当一个线程由于某些原因无法获得所需资源而阻塞时,它的状态为阻塞状态。这可能是因为它正在等待磁盘或网络I / O操作、等待锁或等待其他线程完成等。
  5. 等待(Waiting):当一个线程通过调用wait()方法进入无限期等待状态时,它的状态为等待状态,这通常是在等待另一个线程通知所等待的条件已被满足。
  6. 超时等待(Timed Waiting):当一个线程通过调用带有超时参数的sleep()、wait()或join()方法等待一段时间后进入此状态,它的状态为超时等待状态。
  7. 终止(Terminated):当一个线程已经完成它的任务并退出运行时,或者因未处理异常而突然退出运行时,它的状态为终止状态。

这些不同的线程状态可以在多线程应用程序中解决并发问题。了解线程的状态和各种状态之间的转换是编写高效且正确的多线程代码的关键。

现场同步

线程现场同步是指在多线程编程中,为了保证多个线程访问共享资源时的正确性,需要对这些资源进行加锁和解锁的操作。

当多个线程同时访问同一个共享资源时,如果没有进行同步处理,就可能出现数据不一致、死锁等问题。因此,在多线程编程中,需要使用一些同步机制来控制线程的并发访问。

常见的线程同步机制包括:

  1. 互斥锁(Mutex):用于保护临界区,同一时间只能有一个线程进入该区域。

  2. 读写锁(Read-Write Lock):允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。

  3. 信号量(Semaphore):用于限制同时访问某个资源的线程数量。

  4. 原子操作(Atomic Operations):可以保证多个线程之间的数据操作是原子性的,从而避免了数据竞争的问题。

通过使用适当的线程同步机制,可以有效地避免多线程编程中的数据竞争和死锁等问题,提高程序的可靠性和稳定性。

线程通信

线程通信问题是指在多线程编程中,不同线程之间如何进行通信和协调。由于多个线程可以同时运行,因此如果没有进行适当的同步和互斥操作,就可能出现数据竞争、死锁等问题。

以下是一些常见的线程通信问题:

  1. 竞态条件(Race Condition):当多个线程同时访问共享资源时,如果没有进行同步控制,就可能出现竞态条件。例如,两个线程同时对一个计数器进行递增操作,就可能导致计数器的值不正确。

  2. 死锁(Deadlock):当多个线程相互等待对方释放资源时,就可能出现死锁。例如,线程A持有资源R1并等待获取资源R2,同时线程B持有资源R2并等待获取资源R1,就会出现死锁。

  3. 饥饿(Starvation):当某个线程一直被其他线程抢占资源而无法获得足够的资源时,就可能出现饥饿。例如,线程A持有资源R1并等待获取资源R2,但同时线程B持有资源R2并一直占用,导致线程A一直无法获取资源R2。

为了解决线程通信问题,可以使用一些同步机制和工具,如互斥锁、读写锁、信号量、原子操作等。此外,还需要注意避免循环依赖、共享数据结构等问题,以确保程序的正确性和稳定性。

高级主题

  1. 线程池:线程池是一种管理和重用线程的机制。它可以提高程序的性能和稳定性,因为线程池可以控制并发线程的数量,避免线程过多导致系统资源耗尽的问题。

  2. 并发集合类:Java提供了一些并发集合类,如ConcurrentHashMap、CopyOnWriteArrayList等,它们可以在多线程环境下安全地进行操作。这些集合类的设计考虑了线程安全和性能之间的平衡。

  3. 原子操作:原子操作是一种不可中断的操作,它可以保证多个线程之间的数据操作是原子性的,从而避免了数据竞争的问题。Java提供了一些原子操作类,如AtomicInteger、AtomicLong等。

  4. 线程调度:线程调度是指操作系统如何选择和管理线程的执行时间。Java使用了一个优先级队列来管理线程的执行时间,称为调度器(Scheduler)。通过调整线程的优先级和调度策略,可以优化程序的性能和响应速度。

  5. 线程安全的容器和算法:在多线程环境下,使用不安全的容器或算法可能导致数据竞争和死锁等问题。因此,需要使用线程安全的容器和算法来保证程序的正确性和稳定性。例如,ConcurrentHashMap、CopyOnWriteArrayList、CountDownLatch等。

如上,就是对于线程的一些概念性描述和解释,我们一起作为了解,下来,我们将会进行展开详细描述!!!

二. 线程创建

image-20230524180243990

在这里,我们需要着重注意一下Runable接口

继承Thread创建线程

创建线程方式一:继承Thread类,重写run()方法,调用开启start开启线程

image-20230524191607361

基础案例一:


package com.success.day06;

public class TestThread1 extends Thread {
    /**
     * @Description 重写run方法
     * @Author IT小辉同学
     * @Date 2023/05/24
     */
    @Override
    public void run() {
        // run方法线程体
        for (int i = 0; i < 20; i++) {
            System.out.println("上天不负有心——————" + i);
        }
    }
    /**
     * @Description main主线程
     * @Author IT小辉同学
     * @Date 2023/05/24
     */
    public static void main(String[] args) {
        //创建一个线程对象
        TestThread1 thread1 = new TestThread1();
        //调用start()开启线程
        thread1.start();
        for (int i = 0; i < 2000; i++) {
            System.out.println("我们都是追梦人——————" + i);
        }
    }
}

基础案例二:
package com.success.day06;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;

/**
 * @Description 多线程下载网络图片
 * @Author IT小辉同学
 * @Date 2023/05/24
 */
public class TestThread2 extends Thread {
    //图片链接地址
    private String imgUrl;
    //文件重命名
    private String imgName;

    public TestThread2(String imgUrl, String imgName) {
        this.imgUrl = imgUrl;
        this.imgName = imgName;
    }

    /**
     * @Description 重写run方法,添加下载任务
     * @Author IT小辉同学
     * @Date 2023/05/24
     */
    @Override
    public void run() {
        ImgDownloader downloader = new ImgDownloader();
        downloader.downloader(imgUrl, imgName);
        System.out.println("成功下载了文件:" + imgName);
    }

    /**
     * @param args
     * @Description 创建三个线程,并发
     * @Author IT小辉同学
     * @Date 2023/05/24
     */
    public static void main(String[] args) {
        TestThread2 t1 = new TestThread2("https://pic.netbian.com/uploads/allimg/161003/105551-14754633518f18.jpg", "哆啦A梦1.png");
        TestThread2 t2 = new TestThread2("https://pic.netbian.com/uploads/allimg/161003/104615-1475462775c01d.jpg", "哆啦A梦2.png");
        TestThread2 t3 = new TestThread2("https://pic.netbian.com/uploads/allimg/161003/104021-14754624214a25.jpg", "哆啦A梦3.png");
        t1.start();
        t2.start();
        t3.start();
    }
}

/**
 * @Description 图片下载器
 * @Author IT小辉同学
 * @Date 2023/05/24
 */
class ImgDownloader {
    /**
     * @param url  链接
     * @param name 名字
     * @Description 图片下载方法
     * @Author IT小辉同学
     * @Date 2023/05/24
     */
    public void downloader(String url, String name) {
        try {
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("文件下载异常!!!");
        }
    }
}

注:此处引入了一个依赖包

<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>
image-20230524223718088

通过运行实例我们发现,并不是按照1,2,3的顺序去下载蓝胖子,而是随机的,可见这是一个多线程并发实例!

最后,对于代码结构进行逐行剖析

  1. 定义了一个名为TestThread2的类,继承自Thread类。
  2. 在类中定义了两个成员变量:imgUrl表示图片链接地址,imgName表示文件重命名。
  3. 提供了一个构造方法,接收imgUrl和imgName参数。
  4. 在run方法中创建了一个ImgDownloader类的实例downloader。
  5. 在downloader方法中调用FileUtils的copyURLToFile方法下载网络图片到本地文件中,并打印出成功下载的信息。
  6. main方法中创建了三个TestThread2对象,分别传入不同的url和filename参数,然后调用start方法启动线程。

实现Runnable接口创建线程

image-20230524224558204

基础案例一:
package com.success.day06;

/**
 * @Description 实现Runnable接口,重写 run 方法
 * @Author IT小辉同学
 * @Date 2023/05/24
 */
public class TestRunnable1 implements Runnable {
    /**
     * @Description 重写run方法
     * @Author IT小辉同学
     * @Date 2023/05/24
     */
    @Override
    public void run() {
        // run方法线程体
        for (int i = 0; i < 20; i++) {
            System.out.println("上天不负有心——————" + i);
        }
    }

    /**
     * @Description main主线程
     * @Author IT小辉同学
     * @Date 2023/05/24
     */
    public static void main(String[] args) {
        //创建实现Runnable接口的实现类对象
        TestRunnable1 testRunnable1 = new TestRunnable1();
        //创建一个线程对象,代理
        Thread thread = new Thread(testRunnable1);
        //调用start()开启线程
        thread.start();
        //如上两句可精简为
        //new Thread((testRunnable1)).start();
        for (int i = 0; i < 2000; i++) {
            System.out.println("我们都是追梦人——————" + i);
        }
    }
}

基础案例二:
package com.success.day06;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;

/**
 * @Description 多线程下载网络图片
 * @Author IT小辉同学
 * @Date 2023/05/24
 */
public class TestRunnable2 implements Runnable {
    //图片链接地址
    private String imgUrl;
    //文件重命名
    private String imgName;

    public TestRunnable2(String imgUrl, String imgName) {
        this.imgUrl = imgUrl;
        this.imgName = imgName;
    }

    /**
     * @Description 重写run过程,添加下载任务
     * @Author IT小辉同学
     * @Date 2023/05/24
     */
    @Override
    public void run() {
        ImgDownloader downloader = new ImgDownloader();
        downloader.downloader(imgUrl, imgName);
        System.out.println("成功下载了文件:" + imgName);
    }

    /**
     * @param args
     * @Description 创建三个线程,并发
     * @Author IT小辉同学
     * @Date 2023/05/24
     */
    public static void main(String[] args) {
        TestRunnable2 t1 = new TestRunnable2("https://pic.netbian.com/uploads/allimg/161003/105551-14754633518f18.jpg", "哆啦A梦1.png");
        TestRunnable2 t2 = new TestRunnable2("https://pic.netbian.com/uploads/allimg/161003/104615-1475462775c01d.jpg", "哆啦A梦2.png");
        TestRunnable2 t3 = new TestRunnable2("https://pic.netbian.com/uploads/allimg/161003/104021-14754624214a25.jpg", "哆啦A梦3.png");
        new Thread(t1).start();
        new Thread(t2).start();
        new Thread(t3).start();
    }
}

/**
 * @Description 图片下载器
 * @Author IT小辉同学
 * @Date 2023/05/24
 */
class ImgDownloader {
    /**
     * @param url  链接
     * @param name 名字
     * @Description 图片下载方法
     * @Author IT小辉同学
     * @Date 2023/05/24
     */
    public void downloader(String url, String name) {
        try {
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("文件下载异常!!!");
        }
    }
}

image-20230524230653156

注意:与第一种创建线程的方法进行对比,总结,得出自己的结论!

image-20230524230911092

提升案例一:
package com.success.day06;

/**
 * @Description 模拟多线程并发火车票售票
 * @Author IT小辉同学
 * @Date 2023/05/24
 */
public class TestRunnable3 implements Runnable{
    //火车票总数
    private int ticketNums=20;
    @Override
    public void run() {
        while (true){
            if (ticketNums<=0){
                break;
            }
            //模拟延时
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName()+"获得第"+ticketNums--+"张火车票!!!");
        }
    }

    public static void main(String[] args) {
        TestRunnable3 ticket=new TestRunnable3();
        new Thread(ticket,"IT小辉同学001").start();
        new Thread(ticket,"IT小辉同学002").start();
        new Thread(ticket,"IT小辉同学003").start();
        new Thread(ticket,"IT小辉同学004").start();
        new Thread(ticket,"IT小辉同学005").start();
        new Thread(ticket,"IT小辉同学006").start();
    }
}

image-20230524232642675

这里我们就会发现多线程高并发产生了线程安全问题,后面我们解决这个问题,这里主要演示一下Runnable接口的线程实现方法!!!

提升案例二:

image-20230524233632633

package com.success.day06;

/**
 * @Description 模拟龟兔赛跑
 * @Author IT小辉同学
 * @Date 2023/05/24
 */
public class TestRunnable4 implements Runnable{
    private static String winner;
    @Override
    public void run() {
        for (int i = 0; i <=100 ; i++) {
            //模拟兔子睡觉
            if (Thread.currentThread().getName().equals("兔子")&&i%20==0){
                try {
                    //睡眠时间根据自己笔记本性能自定义
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            //判断比赛是否结束,结束停止比赛
            boolean flag=RaceGame(i);
            if (flag){
                break;
            }
            System.out.println(Thread.currentThread().getName()+"——跑了"+i+"步!!!");
        }
    }

    /**
     * @param steps 步数
     * @return boolean
     * @Description 游戏规则
     * @Author IT小辉同学
     * @Date 2023/05/24
     */
    private boolean RaceGame(int steps){
        //判断是否有胜利者
        if (winner!=null){
            return true;
        }else  {
            if (steps>=100){
                winner=Thread.currentThread().getName();
                System.out.println(winner+"胜出!!!");
                return true;
            }
        }
        return  false;
    }

    public static void main(String[] args) {
        TestRunnable4 race=new TestRunnable4();
        new Thread(race,"兔子").start();
        new Thread(race,"乌龟").start();
    }
}

image-20230524235941762

实现Callable接口创建线程

image-20230525001905270

基础案例一:
package com.success.day07;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;

/**
 * @Description 多线程下载网络图片
 * @Author IT小辉同学
 * @Date 2023/05/24
 */
public class TestCallable1 implements Callable<Boolean> {
    //图片链接地址
    private String imgUrl;
    //文件重命名
    private String imgName;

    public TestCallable1(String imgUrl, String imgName) {
        this.imgUrl = imgUrl;
        this.imgName = imgName;
    }

    /**
     * @Description 重写run过程,添加下载任务
     * @Author IT小辉同学
     * @Date 2023/05/24
     */
    @Override
    public Boolean call() {
        ImgDownloader downloader = new ImgDownloader();
        downloader.downloader(imgUrl, imgName);
        System.out.println("成功下载了文件:" + imgName);
        return true;
    }

    /**
     * @param args
     * @Description 创建三个线程,并发
     * @Author IT小辉同学
     * @Date 2023/05/24
     */
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        TestCallable1 t1 = new TestCallable1("https://pic.netbian.com/uploads/allimg/161003/105551-14754633518f18.jpg", "哆啦A梦1.png");
        TestCallable1 t2 = new TestCallable1("https://pic.netbian.com/uploads/allimg/161003/104615-1475462775c01d.jpg", "哆啦A梦2.png");
        TestCallable1 t3 = new TestCallable1("https://pic.netbian.com/uploads/allimg/161003/104021-14754624214a25.jpg", "哆啦A梦3.png");
        //创建服务(3个线程池)
        ExecutorService service = Executors.newFixedThreadPool(3);
        //提交执行
        Future<Boolean> r1 = service.submit(t1);
        Future<Boolean> r2 = service.submit(t2);
        Future<Boolean> r3 = service.submit(t3);
        //获取结果
        boolean rs1 = r1.get();
        boolean rs2 = r2.get();
        boolean rs3 = r3.get();
        //关闭服务
        service.shutdownNow();
    }
}

/**
 * @Description 图片下载器
 * @Author IT小辉同学
 * @Date 2023/05/24
 */
class ImgDownloader {
    /**
     * @param url  链接
     * @param name 名字
     * @Description 图片下载方法
     * @Author IT小辉同学
     * @Date 2023/05/24
     */
    public void downloader(String url, String name) {
        try {
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("文件下载异常!!!");
        }
    }
}

三. 线程状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LScRJa08-1685088324037)(C:/Users/ASUS/AppData/Roaming/Typora/typora-user-images/image-20230525102658009.png)]

image-20230525102754913

image-20230525102939765

线程停止

image-20230525103229634

基础案例一:
package com.success.day07;

/**
 * @Description 测试线程终止
 * 1.建议线程正常停止 ——> 利用次数,减少使用死循环
 * 2.建议使用标志位 ——>设置一个标志位
 * 3.不要使用stop或者destory等过时或者jdk不建议使用的方法
 * @Author IT小辉同学
 * @Date 2023/05/25
 */
public class TestStop1 implements Runnable {
    //1.设置一个标识位
    private boolean flag = true;

    @Override
    public void run() {
        int i = 0;
        while (flag) {
            System.out.println("run。。。。Thread" + i++);
        }
    }

    //2.设置一个公开的方法停止线程,转换标志位
    public void stop() {
        this.flag = false;
    }

    public static void main(String[] args) {
        TestStop1 testStop1 = new TestStop1();
        new Thread(testStop1).start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("主方法" + i + "次");
            if (i == 900) {
                //调用stop方法切换标志位,让线程停止
                testStop1.stop();
                System.out.println("测试线程终止");
            }
        }
    }
}

主方法899次
主方法900次
run。。。。Thread1632
测试线程终止
主方法901次
主方法902次
主方法903次

线程休眠

image-20230525105449411

基础案例一:
package com.success.day07;

import org.junit.Test;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @Description 延时案例
 * @Author IT小辉同学
 * @Date 2023/05/25
 */
public class TestSleep1 {
    /**
     * @Description 打印系统时间
     * @Author IT小辉同学
     * @Date 2023/05/25
     */
    @Test
    public void systemTime() {
        //获取系统时间
        Date date = new Date(System.currentTimeMillis());
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(new SimpleDateFormat("HH:mm:ss").format(date));
            date = new Date(System.currentTimeMillis());
        }
    }

    /**
     * @Description 倒计时
     * @Author IT小辉同学
     * @Date 2023/05/25
     */
    @Test
    public void tenDown() {
        int num = 10;
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("倒计时:" + num--);
            if (num <= 0) {
                break;
            }
        }
    }
}

线程礼让

image-20230525111558937

基础案例一:
package com.success.day08;

/**
 * @Description 线程礼让
 * @Author IT小辉同学
 * @Date 2023/05/25
 */
public class TestYield  {
    public static void main(String[] args) {
        MyYield myYield=new MyYield();
        new Thread(myYield,"A").start();
        new Thread(myYield,"B").start();
    }
}
class  MyYield implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程开始执行!!!");
        //线程礼让
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"线程执行结束!!!");
    }
}

image-20230525211210476

我们发现,A线程开始之后,没有执行结束,礼让给了B线程!!!

了解即可!!!

四. 线程插队

image-20230525213033239

基础案例一:
package com.success.day08;

/**
 * @Description 线程插队
 * @Author IT小辉同学
 * @Date 2023/05/25
 */
public class TestJoin1 implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i <=1000 ; i++) {
            System.out.println("VIP线程"+i+"插个队!!!");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //启动线程
        TestJoin1 testJoin1=new TestJoin1();
        Thread thread=new Thread(testJoin1);
        thread.start();
        //主线程
        for (int i = 0; i <=500 ; i++) {
            if (i==20){
                thread.join();
            }
            System.out.println("主线程"+i+"执行中!!!");
        }
    }
}

image-20230525214853625

五. 线程状态

image-20230525214956547

详见Java API手册(已上传个人资源,免费领取,不消费积分)

image-20230525215259163

基础案例一:
package com.success.day08;

/**
 * @Description 检测线程状态
 * @Author IT小辉同学
 * @Date 2023/05/25
 */
public class TestState1 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("==================================================");
        });
        //观察线程状态
        Thread.State state = thread.getState();
        System.out.println("线程状态:" + state);
        //观察启动后
        thread.start();
        state = thread.getState();
        System.out.println("线程状态:" + state);
        //只要线程不停止,一直输出状态
        while (state != Thread.State.TERMINATED) {
            Thread.sleep(100);
            state = thread.getState();
            System.out.println("线程状态:" + state);
        }
    }
}

多聊一点,我们再入得深一点,当做一个总结

Java线程状态是指一个线程在其生命周期内所处的状态,Java线程共有6种状态,分别为新建(New)、运行(Runnable)、阻塞(Blocked)、等待(Waiting)、计时等待(Timed Waiting)和终止(Terminated)。

以下是各种状态的详细介绍及代码示例:

  1. 新建状态(New):当使用new关键字或者通过反射创建一个Thread对象时,该线程处于新建状态。此时程序还没有开始执行run()方法。
Thread thread = new Thread();  //创建一个新线程
  1. 运行状态(Runnable):当调用start()方法后,线程进入就绪状态,等待CPU调度它的时间片并开始执行run()方法。运行状态也包括正在执行的状态。
Thread thread = new Thread(){
   @Override
   public void run() {
       while(true){
           System.out.println("正在执行");
       }
   }
};
thread.start();  //调用start()方法转换为运行状态
  1. 阻塞状态(Blocked):从运行状态变为阻塞状态,主要原因是线程被某些锁对象(如synchronized同步块)所占用,其他线程无法访问这些锁对象而被挂起等待。阻塞状态分为两种:等待阻塞和同步阻塞。
  • 等待阻塞:线程执行了wait()、join()、sleep()等方法而进入的状态。
  • 同步阻塞:线程试图获取一个对象锁,而该锁被其他线程占用时,由同步器自动将线程转化为该状态。
Object lock = new Object();
Thread thread1 = new Thread(){
   @Override
   public void run() {
       synchronized (lock){
           try {
               Thread.sleep(5000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
   }
};
Thread thread2 = new Thread(){
   @Override
   public void run

image-20230525220503405

六. 线程优先级

image-20230525220647526

基础案例一:
package com.success.day08;

/**
 * @Description 测试线程优先级
 * @Author IT小辉同学
 * @Date 2023/05/25
 */
public class TestPriority1 {
    public static void main(String[] args) {
        //主线程默认优先级
        System.out.println(Thread.currentThread().getName()+"——>"+Thread.currentThread().getPriority());
        MyPriority myPriority=new MyPriority();
        Thread t1=new Thread(myPriority);
        Thread t2=new Thread(myPriority);
        Thread t3=new Thread(myPriority);
        Thread t4=new Thread(myPriority);
        Thread t5=new Thread(myPriority);
        Thread t6=new Thread(myPriority);
        //设置优先级
        t1.start();

        t2.setPriority(1);
        t2.start();

        t3.setPriority(4);
        t3.start();

        t4.setPriority(Thread.MAX_PRIORITY); //最大为10
        t4.start();

        t5.setPriority(8);
        t5.start();

        t6.setPriority(6);
        t6.start();
    }

}

class MyPriority implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "——>" + Thread.currentThread().getPriority());
    }
}

main——>5
Thread-0——>5
Thread-3——>10
Thread-2——>4
Thread-4——>8
Thread-5——>6
Thread-1——>1

多聊一点

线程优先级是操作系统用来决定哪个线程应该先执行的一种调度策略。在Java中,可以通过设置线程的优先级来影响线程的执行顺序。

Java中的线程分为三个优先级:低、中、高。默认情况下,新创建的线程的优先级为低。

低优先级的线程通常具有较低的执行优先级,它们会在其他线程之前执行。高优先级的线程则具有较高的执行优先级,它们会比低优先级的线程更早执行。

在Java中,可以使用Thread类的setPriority()方法来设置线程的优先级。例如,以下代码将创建一个低优先级的线程:


Thread thread = new Thread(new Runnable() {
   public void run() {
       // 线程代码
   }
});
thread.setPriority(Thread.MIN_PRIORITY);
thread.start();


需要注意的是,虽然设置线程的优先级可以影响线程的执行顺序,但它并不能保证线程一定会被优先执行。操作系统仍然需要根据一些因素(如系统负载、进程状态等)来决定哪些线程应该被优先执行。

七. 守护线程

image-20230525222946716

Java中的守护线程(Daemon Thread)是一种特殊类型的线程,它和普通线程的唯一区别就在于,当所有非守护线程都结束时,虚拟机会自动退出,这也意味着,守护线程的生命周期并不受到其他非守护线程是否执行完毕的影响。

创建一个守护线程和创建普通线程的方式完全相同,只需在使用Thread类创建线程对象之后,通过调用Thread类的setDaemon(true)方法将线程设置为守护线程即可。需要注意的是,setDaemon()方法必须在start()方法被调用之前调用,否则会抛出IllegalThreadStateException异常。

守护线程可以用于实现后台任务,比如垃圾回收器、内存管理器等,所以Java虚拟机中有很多守护线程,它们的优先级比较低,通常情况下,守护线程不应该直接干涉用户线程的工作,比如修改用户线程的数据、与用户线程进行通信等操作。

需要注意的是,守护线程不应该用来执行一些持久性的或者需要确保数据安全的任务,因为在虚拟机退出时,守护线程的finally块中的代码可能永远无法执行,这也就可能导致一些数据不完整的情况。此外,如果将一个正在运行中的线程设置为守护线程,该线程仍会继续执行直到完成其任务或被强制杀死。

总之,Java中的守护线程是一种特殊类型的线程,由虚拟机自动管理和控制,主要用于执行一些后台任务,但在使用时需要考虑到安全性、数据完整性等因素。

基础案例一:
public class DaemonThreadExample {
    public static void main(String[] args) {
        Thread daemonThread = new Thread(new DaemonTask());
        daemonThread.setDaemon(true);
        daemonThread.start();

        // 模拟执行其他任务
        try {
            Thread.sleep(5000); // 等待5秒钟
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("所有线程执行完毕,程序退出!");
    }

    private static class DaemonTask implements Runnable {
        @Override
        public void run() {
            while (true) {
                System.out.println("当前时间:" + new Date());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

在上面的代码中,我们创建了一个名为DaemonThreadExample的类,在其主方法中,首先创建了一个守护线程,并将其设置为守护线程。然后等待5秒钟模拟执行其他任务。最后打印一条消息表示所有线程执行结束。

DaemonTask中,我们使用一个死循环来不断输出当前时间,每输出一次就休眠1秒钟。由于守护线程的生命周期只和其他非守护线程有关,所以即使主线程执行结束后,守护线程仍会继续输出时间,直到进程结束。

八. 线程同步

线程同步是多线程编程中经常遇到的问题,由于多个线程同时运行会访问共享的资源,因此可能会导致一些不可预期的结果。具体来说,当多个线程同时访问同一个共享变量时,如果其中一个线程修改了该变量,那么就可能导致其他线程无法正确地读取该变量的值,从而出现错误。

线程同步的作用就在于解决了这个问题,通过实现对共享变量的访问控制,保证每次只有一个线程能够访问该变量,从而保证数据的一致性和正确性。

Java提供了多种机制来实现线程同步,比较常见的包括以下几种:

  1. synchronized关键字:可以用于方法和代码块,它可以锁定当前对象,保证同一时间只有一个线程执行该对象的方法或代码块。

  2. ReentrantLock类:也可以用于锁定当前对象,其与synchronized关键字的区别在于,它提供了更精细的锁控制,可以实现公平锁、非公平锁等多种锁类型,并且可以在某些情况下减少死锁的发生率。

  3. volatile关键字:可以保证变量的可见性,即使多个线程同时修改变量的值,也不会出现脏读的问题。

  4. Atomic包下的原子类:可以保证某些操作的原子性,即每次只有一个线程能够执行该操作,保证了数据的正确性。

需要注意的是,线程同步可能会带来一定的性能开销,因此在使用时需要根据情况评估是否真正需要同步,并选择合适的同步机制。此外,在进行线程同步时一定要防止死锁等问题的发生。

总之,线程同步是多线程编程中必须面对的问题,通过合理的同步机制可以确保数据的一致性和正确性,并提高系统的稳定性和可靠性。

image-20230525224420802

image-20230525231450555

缺陷:方法里面需要修改的内容才需要锁,锁的太多,浪费资源!!!

image-20230525231801584

基础案例一:

以下是两个售票例子:

  1. 线程不安全的售票例子(未解决线程同步问题)

     public class TicketSeller {
         private int tickets = 50;
    
         public void sellTickets() {
             if (tickets > 0) {
                 System.out.println(Thread.currentThread().getName() + " is selling ticket " + tickets);
                 tickets--;
             }
         }
    
         public static void main(String[] args) {
             TicketSeller seller = new TicketSeller();
    
             // 创建3个窗口
             for (int i = 1; i <= 3; i++) {
                 new Thread(() -> {
                     while (seller.tickets > 0) {
                         seller.sellTickets();
                     }
                 }, "Window" + i).start();
             }
         }
     }
    

    在这个例子中,有一个 TicketSeller 类代表售票系统,该系统共有 50 张票要售卖,同时创建了三个线程模拟窗口,每个窗口可以售卖所有的票。

    但由于没有考虑线程同步问题,会出现一些问题。在卖最后几张票时可能会出现重复出票或漏票的情况,多个窗口有可能同时读取 tickets 变量并且都认为它们可以出售票,因此重复出售或者出售数量不是 50 张票。

  2. 解决线程同步问题的售票例子

     public class TicketSeller {
         private volatile int tickets = 50;
    
         public synchronized void sellTickets() {
             if (tickets > 0) {
                 System.out.println(Thread.currentThread().getName() + " is selling ticket " + tickets);
                 tickets--;
             }
         }
    
         public static void main(String[] args) {
             TicketSeller seller = new TicketSeller();
    
             // 创建3个窗口
             for (int i = 1; i <= 3; i++) {
                 new Thread(() -> {
                     while (true) {
                         seller.sellTickets();
                         if (seller.tickets == 0) {
                             break;
                         }
                     }
                 }, "Window" + i).start();
             }
         }
     }
    

    在这个例子中,我们使用了 synchronized 同步方法来避免多个线程同时修改 tickets 变量,从而避免并发错误。

    由于 synchronized 锁定的是整个对象,因此只有一个线程可以在同一时间访问该对象的同步方法。在这个例子中,卖票操作被定义为同步方法(即使用 synchronized 关键字进行修饰),这样在同时有多个线程执行卖票操作的时候,会保证同一时间仅有一个线程执行卖票操作,从而避免了出现重复出票或者漏票等并发问题。

基础案例二:

以下是两个银行取款案例示例:

  1. 线程不安全的取款案例(未解决线程同步问题)

     public class Account {
         private double balance;
    
         public Account(double balance) {
             this.balance = balance;
         }
    
         public double getBalance() {
             return balance;
         }
    
         public void withdraw(double amount) {
             if (balance >= amount) {
                 balance -= amount;
                 System.out.println(Thread.currentThread().getName() + " withdraws " + amount + ", remaining balance: " + balance);
             } else {
                 System.out.println(Thread.currentThread().getName() + " withdraws fails because of insufficient balance");
             }
         }
    
         public static void main(String[] args) {
             Account account = new Account(1000);
    
             // 创建2个线程模拟取款,取款金额相互重叠
             new Thread(() -> {
                 for (int i = 0; i < 10; i++) {
                     account.withdraw(100);
                 }
             }, "A").start();
    
             new Thread(() -> {
                 for (int i = 0; i < 10; i++) {
                     account.withdraw(100);
                 }
             }, "B").start();
         }
     }
    

    在这个例子中,Account 类代表账户类,其中 withdraw 方法用于从账户余额中取款,同时创建了两个线程模拟用户取款,取款金额相互重叠。

    withdraw 方法并没有考虑到线程安全性问题,当多个线程同时进行取款操作时,会发生并发问题并且导致账户余额异常。

  2. 解决线程同步问题的取款案例

     public class Account {
         private double balance;
    
         public Account(double balance) {
             this.balance = balance;
         }
    
         public synchronized void withdraw(double amount) {
             if (balance >= amount) {
                 balance -= amount;
                 System.out.println(Thread.currentThread().getName() + " withdraws " + amount + ", remaining balance: " + balance);
             } else {
                 System.out.println(Thread.currentThread().getName() + " withdraws fails because of insufficient balance");
             }
         }
    
         public static void main(String[] args) {
             Account account = new Account(1000);
    
             // 创建2个线程模拟取款,取款金额相互重叠
             new Thread(() -> {
                 for (int i = 0; i < 10; i++) {
                     account.withdraw(100);
                 }
             }, "A").start();
    
             new Thread(() -> {
                 for (int i = 0; i < 10; i++) {
                     account.withdraw(100);
                 }
             }, "B").start();
         }
     }
    

    在这个例子中,我们使用了 synchronized 关键字来实现同步方法,保证每个线程顺序执行,并避免多个线程同时访问共享资源(账户余额),导致并发错误。

    withdraw 方法前加上 synchronized 关键字,使得该方法能够被同一时刻只有一个线程访问,防止不同的线程之间对账户进行竞争操作。通过这种方式,每次只有一个线程能够进行取款操作,避免了出现重复提取或者不足等并发问题。

九. 线程锁

线程死锁

image-20230526133431451

基础案例一:
public class DeadlockExample {
    private static final Object LOCK1 = new Object();
    private static final Object LOCK2 = new Object();

    public static void main(String[] args) {
        // 创建第一个线程,获取 LOCK1 后等待 LOCK2
        Thread thread1 = new Thread(() -> {
            synchronized (LOCK1) {
                System.out.println("Thread 1: Acquired LOCK1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (LOCK2) {
                    System.out.println("Thread 1: Acquired LOCK2");
                }
            }
        });

        // 创建第二个线程,获取 LOCK2 后等待 LOCK1
        Thread thread2 = new Thread(() -> {
            synchronized (LOCK2) {
                System.out.println("Thread 2: Acquired LOCK2");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (LOCK1) {
                    System.out.println("Thread 2: Acquired LOCK1");
                }
            }
        });

        // 启动两个线程
        thread1.start();
        thread2.start();
    }
}

在这个例子中,我们创建了两个线程(thread1thread2),分别持有 LOCK1LOCK2 并互相等待对方释放自己所需的锁。当两个线程互相等待对方释放资源时,就会造成死锁现象。

具体而言,thread1 首先获得了 LOCK1,但在尝试获取 LOCK2 时被阻塞。同时,thread2 等待获取 LOCK2,但无法获取到它,因为 LOCK2thread1 持有。因此,两个线程在互相等待对方释放锁资源的情况下进入了死锁状态,无法继续执行后续代码。

基础案例二:

死锁案例

package com.success.day09;

/**
 * @Description 死锁演示:线程之间形成资源冲突
 * @Author IT小辉同学
 * @Date 2023/05/26
 */
public class DeadLock1 {
    public static void main(String[] args) {
        MakeUp g1=new MakeUp(0,"灰姑娘");
        MakeUp g2=new MakeUp(1,"白雪公主");
        g1.start();
        g2.start();
    }
}

/**
 * @Description 口红
 * @Author IT小辉同学
 * @Date 2023/05/26
 */
class Lipstick {

}

/**
 * @Description 镜子
 * @Author IT小辉同学
 * @Date 2023/05/26
 */
class Mirror {

}

class MakeUp extends Thread {
    //资源限制只有一份,使用static进行限制
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();
    int choice; //选择对象
    String person; //使用的对象

    MakeUp(int choice, String person) {
        this.choice = choice;
        this.person = person;
    }

    @Override
    public void run() {
        //化妆
        try {
            makeup();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * @Description 化妆, 互相持有对方的资源,导致资源·锁死
     * @Author IT小辉同学
     * @Date 2023/05/26
     */
    private void makeup() throws InterruptedException {
        if (choice == 0) {
            //获得口红的锁
            synchronized (lipstick) {
                System.out.println(this.person + "获得口红的锁");
                Thread.sleep(1000);
                //一秒后获得镜子的锁
                synchronized (mirror) {
                    System.out.println(this.person + "获得镜子的锁");
                }
            }
        } else {
            //获得镜子的锁
            synchronized (mirror) {
                System.out.println(this.person + "获得镜子的锁");
                Thread.sleep(1000);
                //一秒后获得口红的锁
                synchronized (lipstick) {
                    System.out.println(this.person + "获得口红的锁");
                }
            }
        }
    }
}

解除死锁

package com.success.day09;

/**
 * @Description 死锁演示:线程之间形成资源冲突
 * @Author IT小辉同学
 * @Date 2023/05/26
 */
public class DeadLock1 {
    public static void main(String[] args) {
        MakeUp g1=new MakeUp(0,"灰姑娘");
        MakeUp g2=new MakeUp(1,"白雪公主");
        g1.start();
        g2.start();
    }
}

/**
 * @Description 口红
 * @Author IT小辉同学
 * @Date 2023/05/26
 */
class Lipstick {

}

/**
 * @Description 镜子
 * @Author IT小辉同学
 * @Date 2023/05/26
 */
class Mirror {

}

class MakeUp extends Thread {
    //资源限制只有一份,使用static进行限制
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();
    int choice; //选择对象
    String person; //使用的对象

    MakeUp(int choice, String person) {
        this.choice = choice;
        this.person = person;
    }

    @Override
    public void run() {
        //化妆
        try {
            makeup();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * @Description 化妆, 互相持有对方的资源,导致资源·锁死
     * @Author IT小辉同学
     * @Date 2023/05/26
     */
    private void makeup() throws InterruptedException {
        if (choice == 0) {
            //获得口红的锁
            synchronized (lipstick) {
                System.out.println(this.person + "获得口红的锁");
            }
            synchronized (mirror) {
                System.out.println(this.person + "获得镜子的锁");
            }
        } else {
            //获得镜子的锁
            synchronized (mirror) {
                System.out.println(this.person + "获得镜子的锁");
            }
            //一秒后获得口红的锁
            synchronized (lipstick) {
                System.out.println(this.person + "获得口红的锁");
            }
        }
    }
}

image-20230526140254844

LOCK锁

image-20230526150535131

基础案例一:

以下是一个基于 Lock 锁实现的 Java 售票线程案例:

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

public class TicketSeller {
    private int tickets = 100; // 全局可售票数
    private Lock lock = new ReentrantLock();

    public void sell() {
        lock.lock(); // 获取锁
        try {
            if (tickets > 0) { // 如果还有票则进行售票操作
                System.out.println(Thread.currentThread().getName() + " 正在出售第" + (100 - tickets + 1) + "张票");
                tickets--;
            }
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    public static void main(String[] args) {
        TicketSeller ticketSeller = new TicketSeller();

        // 创建4个售票线程,启动它们以卖出所有票
        Thread thread1 = new Thread(() -> {
            while (ticketSeller.tickets > 0) {
                ticketSeller.sell();
            }
        }, "窗口1");

        Thread thread2 = new Thread(() -> {
            while (ticketSeller.tickets > 0) {
                ticketSeller.sell();
            }
        }, "窗口2");

        Thread thread3 = new Thread(() -> {
            while (ticketSeller.tickets > 0) {
                ticketSeller.sell();
            }
        }, "窗口3");

        Thread thread4 = new Thread(() -> {
            while (ticketSeller.tickets > 0) {
                ticketSeller.sell();
            }
        }, "窗口4");

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

在这个例子中,我们通过 Lock 锁来控制并发线程对售票操作的访问。在 sell() 方法中,我们使用了 lock.lock() 获取锁,并在后面使用 lock.unlock() 释放锁。这样一来,任何时刻只有一个线程能够进入到 sell() 方法体内进行售票操作,从而保证了线程安全。

此外,在主函数中我们创建了4个线程作为售票窗口,它们会并发地通过调用 sell() 方法来完成售票。最终,在所有线程运行结束之前,所有售票就能够被成功出售。

image-20230526152732822

十. 线程协作

生产者消费者模式

线程协作生产者消费者模式是一种常见的并发编程模式,用于解决多个线程间协作的问题。在这种模式中,有两类线程:生产者和消费者。

生产者线程负责向共享缓冲区(即一个数据结构)中放置数据,而消费者线程则负责从缓冲区中取走数据进行处理。因此,它们需要通过线程间通信来配合完成任务,从而保证生产者和消费者之间的同步与互斥。

简单来说,线程协作生产者消费者模式包含以下几个特点:

  1. 存在一个共享数据缓冲区,可以是一个队列或者数组等数据结构,即缓冲区;
  2. 生产者线程向缓冲区中不断添加元素,直到缓冲区满了;
  3. 消费者线程从缓冲区中删除元素,处理完后继续恢复删除操作,直到缓冲区为空;
  4. 当缓冲区已经满了,生产者线程无法往里面添加元素,需要等待直到消费者取走了一些元素释放出缓冲区空间为止;
  5. 当缓冲区为空,消费者线程无法再取出元素进行处理,需要等待直到生产者添加了新元素为止;
  6. 生产者线程和消费者线程之间通过线程间通信机制(如 wait()、notify() 等)进行同步与互斥操作,从而实现协作。

线程协作生产者消费者模式能够有效地解决多个线程并发访问共享数据时可能导致的同步问题,并且可以提高系统的处理效率和资源利用率。

image-20230526153032279

image-20230526153432697

image-20230526153317507

解决方式一

image-20230526153519623

解决方式二

image-20230526153844945

基础案例一:

生产者消费者问题是一个经典的线程协作场景,可以描述为生产者生产数据放入共享缓冲区,消费者从缓冲区中取出数据进行消费。其中,生产者和消费者都是独立的线程。

下面是一个Java线程协作的经典案例:

import java.util.LinkedList;

public class ProducerConsumerExample {
    public static void main(String[] args) throws InterruptedException {
        SharedBuffer buffer = new SharedBuffer(2); // 定义共享缓冲区大小
        Thread producerThread = new Thread(new Producer(buffer));
        Thread consumerThread = new Thread(new Consumer(buffer));

        producerThread.start(); // 启动生产者线程
        consumerThread.start(); // 启动消费者线程

        producerThread.join(); // 等待生产者线程结束
        consumerThread.join(); // 等待消费者线程结束
    }
}

// 定义共享缓冲区类
class SharedBuffer {
    private LinkedList<Integer> data; // 缓冲区数据
    private int maxSize; // 缓冲区最大容量
    
    public SharedBuffer(int maxSize) {
        this.data = new LinkedList<>();
        this.maxSize = maxSize;
    }

    // 生产者向共享缓冲区添加数据
    public synchronized void produce(int value) throws InterruptedException {
        while (data.size() == maxSize) { // 判断缓冲区是否已满
            wait();
        }
        data.add(value);
        System.out.println("Produced: " + value);
        notifyAll(); // 唤醒在等待区的线程
    }

    // 消费者从共享缓冲区取出数据
    public synchronized int consume() throws InterruptedException {
        while (data.size() == 0) { // 判断缓冲区是否为空
            wait();
        }
        int value = data.removeFirst();
        System.out.println("Consumed: " + value);
        notifyAll(); // 唤醒在等待区的线程
        return value;
    }
}

// 生产者类,实现Runnable接口
class Producer implements Runnable {
    private SharedBuffer buffer;

    public Producer(SharedBuffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        try {
            for (int i = 1; i <= 10; i++) { // 生产者循环生产数据
                buffer.produce(i);
                Thread.sleep(1000); // 生产完数据后休眠1秒
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// 消费者类,实现Runnable接口
class Consumer implements Runnable {
    private SharedBuffer buffer;

    public Consumer(SharedBuffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        try {
            while (true) { // 消费者不断消费数据
                buffer.consume();
                Thread.sleep(2000); // 消费完数据后休眠2秒
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

上面的代码中,ProducerConsumerExample类是程序的入口,它创建了一个大小为2的共享缓冲区,并创建了一个生产者线程和一个消费者线程,分别执行生产和消费的操作。

SharedBuffer类是共享缓冲区的实现,它定义了生产者向缓冲区添加数据和消费者从缓冲区取出数据的方法,并使用synchronized关键字实现线程同步。

Producer类和Consumer类分别表示生产者和消费者,它们都实现了Runnable接口,并在run()方法中循环执行生产和消费的操作。在生产者和消费者中,使用wait()函数等待条件满足,并使用notifyAll()函数唤醒在等待区的线程,以达到线程协作的目的。

基础案例二:

假设我们有一个产品仓库,存储着各种产品的信息,包括名称、价格、库存等等。我们有一个生产者线程和一个消费者线程,生产者线程负责不断地生成新的产品信息并将其存储到仓库中,而消费者线程则负责从仓库中获取新的产品信息并进行展示。

为了实现线程协作,我们可以使用Java中的CountDownLatch类来控制生产者和消费者的运行。具体的实现流程如下:

  1. 创建一个生产者线程和一个消费者线程,并启动它们。
  2. 在生产者线程中创建一个CountDownLatch对象,并将其初始化为生产者数量+1。
  3. 在生产者线程中不断地生成新的产品信息,并将其存储到仓库中。同时,通过CountDownLatch对象来通知消费者线程可以获取新的产品信息了。
  4. 在消费者线程中创建一个CountDownLatch对象,并将其初始化为消费者数量+1。
  5. 在消费者线程中不断地从仓库中获取新的产品信息,并进行展示。同时,通过CountDownLatch对象来通知生产者线程可以生产新的产品了。
  6. 当仓库中的产品信息被全部获取完毕后,生产者线程和消费者线程都应该停止运行。

以下是Java代码实现:

import java.util.concurrent.CountDownLatch;

public class ProductStockDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        ProductStock productStock = new ProductStock();

        // 创建生产者线程
        for (int i = 0; i < 10; i++) {
            executor.execute(new ProducerThread("Product " + i));
        }

        // 创建消费者线程
        for (int i = 0; i < 10; i++) {
            executor.execute(new ConsumerThread("Product " + i));
        }

        // 启动生产者线程和消费者线程
        executor.shutdown();
        executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
    }
}

class ProducerThread implements Runnable {
    private String name;

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

    @Override
    public void run() {
        try {
            // 创建新产品信息
            Product product = new Product(name, 999);
            // 将产品信息存储到仓库中
            productStock.put(product);
            // 通知消费者线程可以获取新的产品信息了
            productStock.countDown();
            System.out.println("Producer " + name + " created new product " + product.getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ConsumerThread implements Runnable {
    private String name;

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

    @Override
    public void run() {
        try {
            // 从仓库中获取新的产品信息
            Product product = productStock.take();
            // 展示新产品信息
            System.out.println("Consumer " + name + " is handling product " + product.getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

十一. 线程池

image-20230526155925097

image-20230526160120673

基础案例一:

下面是一个Java线程池的经典案例:

import java.util.concurrent.*;

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2); // 创建固定大小为2的线程池
        for (int i = 1; i <= 10; i++) { // 循环提交任务
            executor.execute(new Task(i)); // 提交任务到线程池中
        }
        executor.shutdown(); // 关闭线程池
    }
}

// 自定义任务类
class Task implements Runnable {
    private int taskId;

    public Task(int taskId) {
        this.taskId = taskId;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " is executing task " + taskId);
        try {
            Thread.sleep(1000); // 模拟任务执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

上面的代码中,ThreadPoolExample类创建了一个固定大小为2的线程池,并循环提交10个任务。每个任务都是一个Task对象,它实现了Runnable接口,并在run()方法中模拟了一个耗时1秒的操作。

通过ExecutorService接口和Executors工具类可以轻松创建各种类型的线程池,这里使用的是newFixedThreadPool()方法创建固定大小的线程池。然后,循环提交10个任务到线程池中,线程池会自动调度线程来执行任务。最后,调用shutdown()方法关闭线程池。
Java线程池是提高多线程应用程序性能的重要机制之一。通过它可以有效控制线程数量,避免线程创建和销毁的开销,减少了线程竞争,显著提高了系统运行效率和稳定性。所以,使用Java线程池来管理线程非常重要。

知秋君
上一篇 2024-08-08 21:48
下一篇 2024-08-08 21:12

相关推荐