Java多线程

Java多线程

1、什么是多线程?

指在软件或者硬件中同一时间并行实现多个线程并发执行的技术(即在一个程序中运行多个不同的线程来执行不同的任务)


2、他有什么作用?优缺点有哪些?

作用:

(1)在单核CPU中,将CPU分为很小的时间片,在每一时刻只能有一个线程在执行,是一种微观上轮流占用CPU的机制。由于CPU轮询的速度非常快,所以看起来像是“同时”在执行一样。多线程会存在线程上下文切换,会导致程序执行速度变慢;

(2)多线程不会提高程序的执行速度,反而会降低速度。但是对于用户来说,可以减少用户的等待响应时间,提高了资源的利用效率。

(3)多线程并发利用了CPU轮询时间片的特点,在一个线程进入阻塞状态时,可以快速切换到其余线程执行其余操作,这有利于提高资源的利用率,限度的利用系统提供的处理能力,有效减少了用户的等待响应时间。

优点:

  1. 提高性能:现在计算机绝大多数拥有多核处理器,多线程技术可以充分利用多个核心,同时执行多个不同的任务,大大提升了程序的吞吐率和运行速度;
  2. 共享资源:多个线程可以共享同一进程的内存空间,从而实现线程之间的资源共享;
  3. 并发处理:多线程允许程序同时处理多个任务,从而更加快速有效的处理并发请求、消息和数据;
  4. 异步编程:多线程可用于实现异步编程模型,通过在后台执行任务来提高程序的执行效率;
  5. 模块化设计:使用多线程可以将程序分化为多个不同的模块,每个模块都拥有单独的线程来完成任务,降低了代码之间的耦合度,更加便于管理和维护;
  6. 改善用户体验:使用多线程可以提高程序的运行速度,在GUI程序中可以确保界面响应灵敏,不会因为一个耗时过大的操作浪费时间

缺点:

  1. 线程切换和调度开销:线程的切换和调度需要消耗系统资源,过多的线程切换可能会影响程序性能。
  2. 线程安全问题:多个线程访问共享资源时,可能会出现死锁、竞态条件等安全问题。
  3. 调试和测试难度:由于线程之间相互独立,调试和测试多线程程序可能需要特殊的工具和技巧。
  4. 代码可读性差:多线程程序的代码结构可能变得复杂,影响代码的可读性。
  5. 资源消耗:每个线程都需要占用内存空间和系统资源,过多的线程会增加系统资源的消耗。
  6. 数据同步问题:在多线程编程中,如果不正确地处理数据同步和互斥访问,可能会导致数据竞争程序不稳定

3、他应该怎样实现?创建多线程的方式有哪些?这些方法有什么区别?

可以继承Thread类,或者调用Runnable / Callable接口来实现

  • 继承Thread类:

    // 继承多线程类
    public class TestThread extends Thread{
        @Override
        public void run() {
            // run()函数,多线程执行体
            for (int i = 0; i < 20; i++) {
                System.out.println("i am studying--" + i);
            }
        }
    
        public static void main(String[] args) {
            //main()函数,为主函数
            TestThread testThread = new TestThread();
            testThread.start();
    
            for (int i = 0; i < 200; i++) {
                System.out.println("我在学习多线程--" + i);
            }
        }
    }
    
  • 调用Runnable接口:

    //基础调用方式
    public class TestNew {
        public static void main(String[] args) {
            MyThread2 myThread2 = new MyThread2();//调用Runnable接口需要一个代理对象
            new Thread(myThread2).start(); //需要调用Thread.start()启动线程 
        }
    }
    class MyThread2 implements Runnable{
        @Override
        public void run() {
            System.out.println("这是调用Runnable接口使用线程的方法");
        }
    }
    
    import org.apache.commons.io.FileUtils;
    import java.io.File;
    import java.net.URL;
    
    //多线程下载图片
    //调用 Runnable接口
    public class TestThread2 implements Runnable{
        private String url;
        private String name;
    
        public TestThread2(String url, String name) {
            this.url = url;
            this.name = name;
        }
    
    
        @Override
        public void run() {
            WebDownLoader webDownLoader = new WebDownLoader();
            webDownLoader.downLoader(url, name);
            System.out.println("正在下载,文件下载名为:" + name);
        }
    
        public static void main(String[] args) {
            TestThread2 t1 = new TestThread2("https://imga2.5054399.com/upload_pic/2024/2/26/4399_16291960410.jpg","1.jpg");
            TestThread2 t2 = new TestThread2("https://imga2.5054399.com/upload_pic/2024/3/1/4399_17225675210.jpg","2.jpg");
            TestThread2 t3 = new TestThread2("https://imga1.5054399.com/upload_pic/2017/1/24/4399_10181217218.jpg","3.jpg");
    
            // runnable方法需要 new一个 Thread对象进行
            new Thread(t1).start();
            new Thread(t2).start();
            new Thread(t3).start();
        }
    }
    
    //下载器
    class WebDownLoader {
        public void downLoader(String url, String name) {
            try {
                FileUtils.copyURLToFile(new URL(url), new File(name));
            }catch (Exception e) {
                System.out.println("IO异常,downLoader方法出现问题:"+e);
            }
    
        }
    }
    
  • 调用callable接口:

    public class TestNew {
        public static void main(String[] args) {
            FutureTask<Object> futureTask = new FutureTask<Object>(new MyThread3());
            new Thread(futureTask).start();
            try {
                String s = futureTask.get();
                System.out.println(s);
            }catch (Exception e) {
                System.out.println("" + e);
            }
        }
    }
    class MyThread3 implements Callable<Object> {
    
        @Override
        public Object call() throws Exception {
            return "这是调用callable接口使用线程的方法";
        }
    }
    

三种实现方式的区别:

  1. callable和runnable区别

    • 使用方法不同:

      • Runnable接口只有一个 run( ) 方法,该方法不返回任何值,因此无法抛出任何checked Exception。
      • Callable接口则有一个 call( ) 方法,它可以返回一个值,并且可以抛出一个checked Exception。
    • 返回值不同:

      • Runnable的 run( ) 方法没有返回值,只是一个void类型的方法。
      • Callable的 call( ) 方法却必须有一个返回值,并且返回值的类型可以通过 **泛型 **进行指定。
    • 异常处理不同:

      • 在Runnable中,我们无法对run()方法抛出的异常进行任何处理。
      • 但在Callable中,自定义的call()方法可以抛出一个checked Exception,并由其执行者Handler进行捕获并处理。
    • 使用场景不同:

      • Runnable适用于那些不需要返回值,且不会抛出checked Exception的情况,比如简单的打印输出或者修改一些共享的变量。

      • Callable适用于那些需要返回值或者需要抛出checked Exception的情况,比如对某个任务的计算结果进行处理,或者需要进行网络或IO操作等。

      • 在Java中,常常使用Callable来实现异步任务的处理,以提高系统的吞吐量和响应速度。

    注意点:Callable接口支持返回执行结果,此时需要调用 FutureTask.get() 方法实现,此方法会阻塞主线程直到获取 ”将来“ 的结果;当不调用此方法时,主线程不会阻塞!


4、使用多线程的过程中是否会出现问题?会出现哪些问题?应该怎么解决?

虽然多线程的使用可以带来很多的便利,但同时也会带来一些问题

下面是一些常见问题的代码案例:

  • 多窗口售卖火车票:

    //不安全的买票机制
    //线程不安全,会出现负数
    public class UnsafeBuyTicket {
        public static void main(String[] args) {
            buyTicket buyTicket = new buyTicket();
            new Thread(buyTicket,"苦逼的我").start();
            new Thread(buyTicket,"幸运的你").start();
            new Thread(buyTicket,"天杀的黄牛").start();
        }
    }
    
    class buyTicket implements Runnable{
        //票的数量
        private int ticketNums = 20;
        //判断程序停止的条件
        boolean flag = true;
    
        @Override
        public void run() {
            //抢票
            while (flag) {
                try {
                    rubberyTicket();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        //抢票的方法
        private void rubberyTicket() throws InterruptedException {
            //首先判断票还在不在
            if (ticketNums <= 0) {
                flag = false;
                return;
            }
            //模拟延时
            Thread.sleep(100);
            //抢票
            System.out.println(Thread.currentThread().getName() + "-->抢到了第" + ticketNums-- + "张票");
        }
    }
    

    运行结果:

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

    我们可以看到上面的运行结果不仅出现了抢到重复的票,甚至出现了负数;

  • 银行取钱:

    //不安全银行取钱
    public class UnsafeBank {
    
        public static void main(String[] args) {
            //账户
            Account account = new Account(100, "结婚基金");
    
            Drawing you = new Drawing(account, 50, "你");
            Drawing girlFriend = new Drawing(account, 100, "girlFriend");
    
            you.start();
            girlFriend.start();
        }
    }
    
    //规定一个账户
    class Account {
        int money; //余额
        String name; // 用户名
        public Account(int money, String name) {
            this.money = money;
            this.name = name;
        }
    }
    
    //银行:模拟取款
    class Drawing extends Thread{
        //账户
        Account account;
        //取了多少钱
        private int drawingMoney;
        //还剩多少钱
        private int nowMoney;
    
        public Drawing(Account account,int drawingMoney, String name) {
            super(name);
            this.account = account;
            this.drawingMoney = drawingMoney;
        }
    
        //synchronized默认锁的是 this
        @Override
        public void run() {
                //取钱之前先看看余额够不够取
                if (account.money - drawingMoney < 0) {
                    System.out.println(Thread.currentThread().getName() + "钱不够,取不了");
                    return;
                }
    
                //模拟延时
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                //账户余额的钱 = 账户存款 - 取的钱
                account.money = account.money - drawingMoney;
                //你现在手里的钱 = 手里的钱 + 取的钱
                nowMoney = nowMoney + drawingMoney;
    
                System.out.println(account.name + "余额为:" + account.money);
                System.out.println(this.getName() + "手里的钱为:" + nowMoney);
        }
    }
    

运行结果:

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

通过这个结果我们发现,账户余额也出现了负数的情况;

这两个demo都出现了一些不合理的情况,比如出现抢到 第 -1张票,或是抢到重复的票,或者是余额剩余 -50元,产生这种情况大的原因是由多线程操作共享资源tickets所导致的线程安全问题。在多个线程在执行售票的任务的时候,由于在售票的代码中访问了同一个成员变量tickets。可是在操作tickets的这些语句中,一个线程操作到其中的一部分代码的时候,CPU切换到其他的线程上开始执行代码。这样就会导致tickets变量中的值被修改的不一致。

总结多线程的安全问题发生的原因:

  1. 首先必须有多线程。
  2. 多个线程在操作共享的数据,并且对共享数据有修改。
  3. 本质原因是CPU在处理多个线程的时候,在操作共享数据的多条代码之间进行切换导致的。

多线程可能会出现的问题总结:

  1. 线程安全问题:当多个线程访问同一块共享内存区域时,如果这些访问不是原子的,可能会发生数据的不一致或丢失。这是因为线程可能在访问过程中被暂停或取消,导致对共享资源的未完成操作被其他线程看到已完成的情形。
  2. 性能问题:线程的创建和销毁会对系统内存产生较大的开销。如果线程的数量超过了处理器的核心数量,许多线程可能会处于空闲状态,从而浪费系统的资源和影响垃圾回收过程。此外,频繁的线程创建和销毁会影响程序的执行效率。
  3. 活跃性问题:死锁是一种常见的并发错误,当两个或更多的线程都在争夺同一个资源,并且它们都需要对方释放这个资源时,会发生死锁。另一种问题是饥饿,这是指某个线程或多个线程因无法获得所需资源而长时间得不到执行的情况。
  4. 调度开销:操作系统会根据调度算法为线程分配时间片,以便线程能够有机会执行。这种上下文切换的开销较大,尤其是在线程数量远超过处理器核心数量的场景下。此外,缓存失效也是多线程环境中的一种性能问题,因为线程调度可能导致缓存中的数据不再有效。
  5. 协作开销:在多线程环境中,为了确保数据的一致性和安全性,可能需要牺牲一些性能以避免数据冲突和不一致。例如,为了保证线程安全,编译器和CPU可能不会进行某些优化,或者线程之间的通信操作会增加额外的开销。

既然存在这么多问题,自然会有解决问题的方法

  1. 同步代码块:

    说明:我们了解到线程的安全问题其实就是由多个线程同时处理共享资源所导致的,要想解决线程的安全问题,必须保证下面用于处理共享资源的代码在任何时刻只能被一个线程访问。

    为了实现这种限制,Java中提供了同步机制,当多个线程使用同一个共享资源时,可以将处理共享资源的代码放置在一个代码块中,使用Synchronized关键字来修饰.被称作同步代码块.

    Synchronized(锁对象) {

    }

    • 锁对象是任意的一个对象
    • 资源操作的所有代码需要被同步

    要想保证线程的安全:需要在操作共享数据的地方,加上线程的同步锁。
    锁对象的前提条件 :必须要保证 锁对象的唯一性.

    这样做的好处是, 同步方法被所有线程所共享, 方法所在的对象相对于所有线程来说是唯一的, 从而保证了锁的唯一性. 当一个线程执行该方法时, 其它的线程就不能进入到该方法中, 直到这个线程执行完该方法为止, 从而达到了线程同步的效果.

    以抢票为例:给 rubberyTicket()方法添加一个 synchronized关键字

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

​ 运行结果变为正常,不会出现 第0/-1张票,也不会出现多人抢到同一张票的情况

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

注意:同步代码块中的锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是唯一的.。任意 说的是共享锁对象的类型, 所以,,锁对象的创建代码不能放在 run() 方法中。否则每个线程运行到 run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁, 每个锁都有自己的标志位. 线程之间便不能产生同步的效果。


5、什么是高并发?他会导致那些问题?如何解决高并发问题?

(1)高并发介绍:

高并发(High Concurrency)通常是指通过设计保证系统能够同时并行处理很多请求。通俗来讲,高并发是指系统在同一时间段内处理大量并发请求的能力。

(2)可能会导致的问题:

  1. 资源竞争:多个线程同时访问共享的资源,如数据库连接、文件等,可能导致资源竞争问题,包括数据不一致、死锁等。
  2. 线程安全问题:多个线程同时访问共享的变量或对象时,可能会导致数据错误或不一致的问题。
  3. 性能瓶颈:高并发情况下,系统的性能可能会受到限制,导致响应时间延长或系统崩溃。

(3)解决方法:

  1. 使用线程池:通过线程池来管理线程,控制线程的数量,避免线程创建和销毁的开销,并能限制并发请求数量,避免资源耗尽。
  2. 使用缓存:通过使用缓存技术,将计算结果或数据库查询结果缓存起来,减少对数据库等资源的访问,提高系统的并发处理能力。
  3. 数据库优化:对数据库进行优化,包括索引的设计、查询语句的优化等,提高数据库的并发处理能力。
  4. 使用分布式系统:将系统拆分成多个独立的服务,通过分布式部署来提高系统的并发处理能力。
  5. 使用消息队列:将请求转化为消息,通过消息队列来异步处理请求,减少直接面对高并发带来的压力。
  6. 使用缓存技术:通过使用缓存技术,将计算结果或数据库查询结果缓存起来,减少对数据库等资源的访问,提高系统的并发处理能力。

以上这些方法并非适用于所有场景,具体的解决方案需要根据具体的业务和系统需求来确定。


6、死锁是什么?他的产生原因有哪些?有什么解决办法?

死锁:是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象(就是指两个或多个进程持有对方进程所需要的资源,形成僵持)

例如两个女孩都需要化妆,灰姑娘首先拿到了口红,而白雪公主首先拿到了镜子,但是两人都不愿意让出自己的东西,还想要对方的资源,于是双方相互等待造成死锁,代码如下:

public class deathLock1 {
    public static void main(String[] args) {
        makeup girl1 = new makeup(0,"灰姑娘");
        makeup girl2 = new makeup(1,"白雪公主");

        //启动线程
        girl1.start();
        girl2.start();
    }
}

//化妆

//1. 定义一个口红类
class Lipstick {

}

//2. 定义一个镜子类
class Mirror {

}

//3. 定义一个执行类并继承 Thread方法
class makeup extends Thread {

    //要化妆首先要获取装备,需要添加 static否则不会发生死锁
    //类在 main方法中被 new 了二次,td1和 td2各自分别产生了 o1,o2。
    //如果没有static,创建的就是各个线程各自对象,就没法在线程之间共享了
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    //其次要获取执行对象
    int chance;
    String name;

    public makeup(int chance, String name) {
        this.chance = chance;
        this.name = name;
    }


    @Override
    public void run() {
        //化妆
        try {
            makingUp();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //定义一个化妆的方法
    private void makingUp() throws InterruptedException {
        if (chance == 0) {
            synchronized (lipstick) { // 获取口红的锁
                System.out.println(this.name + "拿到了口红");
                Thread.sleep(1000); // 停止一秒后去拿镜子
                synchronized (mirror) {// 获取镜子的锁
                    System.out.println(this.name + "拿到了镜子");
                }
            }
        }
        else {
            synchronized (mirror) { // 获取镜子的锁
                System.out.println(this.name + "拿到了镜子");
                Thread.sleep(2000); // 停止2秒后去拿口红
                synchronized (lipstick) {// 获取口红的锁
                    System.out.println(this.name + "拿到了口红");
                }
            } 
        }
    }
}

运行结果:

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

* 产生死锁的必要条件:
* 1. 互斥条件:一个资源每次只能被一个进程使用
* 2. 请求与保持条件:一个进程因请求资源在阻塞时,对已获得的资源不放
* 3. 不剥夺条件:进程已获得资源,在未完成时,不可以剥夺资源
* 4. 循环等待条件:在若干个进程之间形成一种头尾相接的循环等待资源关系。

解决死锁的办法:解决上述条件的一个或多个,就可以解决死锁问题

  1. 避免使用多个锁:当多个线程需要获取多个锁时,可以尝试将多个锁合并为一个锁,或者将一个锁拆分为多个锁,以避免死锁的发生。
  2. 保持锁的顺序一致:当多个线程需要获取多个锁时,确保它们获取锁的顺序是一致的,避免不同线程以不同的顺序获取锁而导致死锁。
  3. 设置获取锁的超时时间:在获取锁时设置一个超时时间,如果在指定的时间内无法获取到锁,则放弃当前获取的锁,等待一段时间后重新尝试获取锁。

具体代码实现如下:

(1)使用synchronized关键字:

  • synchronized修饰的方法会自动获取对象的内置锁(也称为同步锁),只有当前线程能够进入该方法并执行其中的代码。其他线程需要等待当前线程释放锁才能进入该方法。这样就确保了每次只有一个线程能够执行被synchronized修饰的代码段,从而保证了线程的安全性。

    修改后代码如下:

     private void makingUp() throws InterruptedException {
            if (chance == 0) {
                synchronized (lipstick) { // 获取口红的锁
                    System.out.println(this.name + "拿到了口红");
                    Thread.sleep(1000); // 停止一秒后去拿镜子
                }
                synchronized (mirror) {// 获取镜子的锁
                    System.out.println(this.name + "拿到了镜子");
                }
            }
            else {
                synchronized (mirror) { // 获取镜子的锁
                    System.out.println(this.name + "拿到了镜子");
                    Thread.sleep(2000); // 停止2秒后去拿口红
                }
                synchronized (lipstick) {// 获取口红的锁
                    System.out.println(this.name + "拿到了口红");
                }
            }
        }
    

    运行结果:

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

    可以看到死锁问题已经得到解决。

(2)使用 ReentrantLock 类:

  • ReentrantLock 提供了更加灵活、功能更强大的锁机制。与synchronized不同,ReentrantLock 支持公平锁和非公平锁两种模式,还可以设置超时等待时间。

    修改后代码如下:

    //定义一个Lock锁
    private final ReentrantLock lock = new ReentrantLock();
    
    @Override
    public void run() {
        while (true) {
            try {
                lock.lock(); // 加锁
                if (ticketNum > 0) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(ticketNum--);
                }else {
                    break;
                }
            } finally {
                lock.unlock(); //解锁
            }
        }
    }
    
* synchronizedlock()锁的区别:
* 1. lock是显式锁,需要手动的开关锁;synchronized是隐式锁,出了作用区域会自动释放
* 2. lock只有代码块锁;synchronized有代码块锁和方法锁
* 3. 使用 lock锁,JVM会花费较少的时间来调度线程,性能更好一点。并且具有良好的可扩展性(有更多的子类)
* 4. 优先使用顺序
*    Lock > 同步代码块(已经进入了方法体,分配了相应的资源)> 同步方法(在方法体之外)
8、三大不安全案例分别是什么?有没有解决办法?

经典的三大多线程不安全案例分别是:

  1. 竞态条件(Race Condition):竞态条件指的是多个线程并发执行时,最终的结果取决于线程的执行顺序。当多个线程同时访问和修改共享的数据时,如果执行顺序不确定或者不正确,可能导致数据的不一致性。例如,多个线程同时对同一个计数器进行自增操作,如果没有适当的同步机制,可能导致计数结果错误。
  2. 死锁(Deadlock):死锁指的是多个线程因为相互等待对方释放资源而无法继续执行的情况。当多个线程同时持有一些共享资源,并且每个线程都在等待其他线程释放它所需要的资源时,就可能发生死锁。如果没有合适的策略来避免死锁或解决死锁,程序将无法继续执行。
  3. 数据竞争(Data Race):数据竞争指的是多个线程同时访问共享的可变数据,并且至少有一个线程对数据进行写操作,而没有适当的同步机制来保护数据的一致性。当多个线程并发读写共享数据时,可能导致数据损坏或不一致。例如,一个线程正在修改某个对象的属性,而另一个线程同时读取该属性的值,可能读取到的是不一致的结果。

针对以上三个不安全的问题,相对应的解决方法如下:

  1. 竞态条件(Race Condition):
    • 使用互斥锁(Mutex)或信号量(Semaphore)等同步机制来保护共享数据的访问,确保同一时间只有一个线程能够修改共享数据。
    • 使用原子操作或原子类型(Atomic Type)来进行数据的读写操作,确保原子性,避免竞态条件的发生。
    • 使用条件变量(Condition Variable)来进行线程之间的等待和通知,以控制线程的执行顺序。
  2. 死锁(Deadlock):
    • 避免循环等待(Circular Wait)的情况,即线程获取资源的顺序应该是一致的,可以通过按照预定的顺序获取资源来避免死锁。
    • 使用资源分级策略,确保线程按照一定的优先级来获取和释放资源,避免出现所有线程都无法继续执行的情况。
    • 引入超时机制,当线程等待资源的时间超过一定阈值时,可以主动释放已持有的资源,避免死锁的发生。
  3. 数据竞争(Data Race):
    • 使用互斥锁(Mutex)或读写锁(Read-Write Lock)来保护共享数据的读写操作,确保同一时间只有一个线程能够修改共享数据。
    • 使用原子操作或原子类型(Atomic Type)来进行数据的读写操作,确保原子性,避免数据竞争的发生。
    • 使用线程局部存储(Thread-Local Storage)来保证每个线程都有自己独立的数据副本,避免共享数据的读写冲突。

9、什么是生产者消费者模式?有那些实现方式?实现方式的优缺点有哪些?这种模式有什么优点?

(1)什么是生产者消费者模式:

生产者消费者模式是一种常见的多线程设计模式,用于解决生产者和消费者之间的数据交换和协作问题。在该模式中,生产者负责生成数据并将其放入共享的缓冲区,而消费者则从缓冲区中取出数据进行处理。

生产者消费者模式的基本思想是将生产者和消费者解耦,使它们能够并发地执行,同时通过共享的缓冲区来进行数据交换,从而提高系统的效率和资源利用率。

以下是生产者消费者模式的基本组件和流程:

  1. 缓冲区(Buffer):用于存储生产者生成的数据,供消费者消费。缓冲区可以是一个队列、一个缓冲池或者其他数据结构。
  2. 生产者(Producer):负责生成数据,并将数据放入缓冲区。生产者可以是一个线程或者多个线程。
  3. 消费者(Consumer):从缓冲区中取出数据,并进行相应的处理。消费者可以是一个线程或者多个线程。

它的基本流程如下:

  • 当缓冲区为空时,消费者等待,直到生产者向缓冲区放入数据。
  • 当缓冲区满时,生产者等待,直到消费者从缓冲区取出数据。
  • 生产者生成数据并放入缓冲区。
  • 消费者从缓冲区取出数据并进行处理。

(2)管程法和信号灯法都是实现生产者消费者模式的经典方法。

  1. 管程法(Monitor):

    • 管程是一种高级的同步机制,它封装了共享数据和操作共享数据的方法。在管程中,通过定义条件变量和操作方法来实现对共享数据的访问和同步。
    • 在生产者消费者模式中,可以使用管程来实现对缓冲区的访问和同步。管程提供了等待和通知的机制,使得生产者和消费者可以在缓冲区满或空的情况下进行等待和唤醒操作,以保证生产者和消费者的正确协作。

    代码如下:

    package com.gaoji;
    
    //测试生产者消费者模型:
    //利用缓存区解决-->管程法
    //生产者,消费者,产品,缓冲区
    public class TestPC {
        public static void main(String[] args) {
            SynContainer container = new SynContainer();
            new Product(container).start();
            new Consumer(container).start();
        }
    }
    
    //生产者
    class Product extends Thread{
        SynContainer container;
        public Product(SynContainer container) {
            this.container = container;
        }
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("生产了" + i + "只鸡");
                container.push(new Chicken(i));
            }
        }
    }
    
    //消费者
    class Consumer extends Thread{
        SynContainer container;
        public Consumer(SynContainer synContainer) {
            this.container = synContainer;
        }
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("吃掉了" + container.pop().id + "只鸡");
                container.pop();
            }
        }
    }
    
    //产品
    class Chicken {
        int id;
        public Chicken(int id) {
            this.id = id;
        }
    }
    
    //缓冲区
    class SynContainer {
        //需要一个容器的大小
        Chicken[] chickens = new Chicken[10];
        //容器的计数器
        int count = 0;
    
        //生产者放入产品
        synchronized void push(Chicken chicken) {
            for (int i = 0; i < 10; i++) {
                if (count == chickens.length) {
                    //如果容器满了,需要等待消费者消费,生产者等待
                    try {
                        this.wait();
                    }catch (Exception e) {
                        System.out.println("SynContainer区类中的Push方法出现异常-->" + e);
                    }
                }
            }
            //如果没有满,则继续放入产品
            chickens[count] = chicken;
            count++;
            //可以通知消费者消费
            //notifyAll()唤醒正在等待此对象监视器锁的所有线程。
            this.notifyAll();
    
        }
    
        //消费者消费产品
        synchronized Chicken pop() {
            //首先判断是否满足消费条件
            if (count == 0) {
                //等待生产者生产,消费者等待
                try {
                    this.wait();
                }catch (Exception e) {
                    System.out.println("SynContainer区类中的Push方法出现异常-->" + e);
                }
            }
            //如果可以消费
            count--;
            Chicken chicken = chickens[count];
            //产品用完了,通知生产者生产
            this.notifyAll();
            return chicken;
        }
    }
    
  2. 信号灯法(Semaphore):

    • 信号灯是一种计数器,用于控制访问共享资源的线程数量。通过对信号灯的操作,可以实现对线程的同步和互斥。
    • 在生产者消费者模式中,可以使用信号灯来控制缓冲区的空槽数量或者已填充的数据数量。生产者和消费者在访问缓冲区之前,需要通过对信号灯进行操作来获取相应的许可(P操作),并在访问完毕后释放许可(V操作)。

代码如下:

//测试生产者消费者 2:信号灯法,标志位解决
public class TestPC2 {
    public static void main(String[] args) {
        Item item = new Item();
        new Actor(item).start();
        new Watcher(item).start();
    }
}

//生产者(演员)
class Actor extends Thread{
    //首先需要一个节目对象
    Item item;
    public Actor(Item item) {
        this.item = item;
    }

    @Override
    public void run() {
        //产出节目
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                this.item.perform("正在播放电视剧。。。");
            }else {
                this.item.perform("广告ing...");
            }
        }
    }
}

//消费者(观众)
class Watcher extends Thread {
    //首先需要一个节目对象
    Item item;
    public Watcher(Item item) {
        this.item = item;
    }
    
    //观众只需要消费产品即可
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            try {
                this.item.watch();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

//产品(节目)
class Item {
    //演员演完了,观众来看 T
    //观众看完了,演员来演 F
    String voice; //表演的节目
    boolean flag = true;

    //演员表演节目
    public synchronized void perform(String voice) {
        //首先判断是否需要表演
        if (!flag) {
            try {
                this.wait();
            }catch (Exception e) {
                System.out.println("" + e);
            }
        }
        //如果没有节目,则演员继续表演节目
        System.out.println("演员表演了:" + voice);
        //表演完了,通知观众观看节目
        this.notifyAll();
        this.voice = voice;
        this.flag = !this.flag;// 更换一下 flag的状态
    }

    //观众看节目
    public synchronized void watch() throws InterruptedException {
        //首先判断是否有节目观看
        if (flag) {
            //无节目,则需暂停,等待演员表演完成
            this.wait();
        }
        System.out.println("观众观看了:" + voice);
        //通知演员表演节目
        this.notifyAll();
        this.flag = !this.flag;
    }
}

(3)两种方法的优缺点:

管程法的优点:

  1. 封装性好:管程将共享数据和操作共享数据的方法封装在一起,使得代码结构清晰、易于理解和维护。
  2. 简洁明了:通过定义条件变量和操作方法,可以直接表达生产者消费者之间的协作关系,代码逻辑清晰。
  3. 安全性高:管程提供了等待和通知的机制,避免了死锁和竞态条件的发生。

管程法的缺点:

  1. 语言依赖性:管程法是一种高级的同步机制,需要编程语言或操作系统提供对管程的支持。不是所有编程语言或平台都直接支持管程。
  2. 功能相对局限:管程法主要用于描述共享数据和操作,对于更复杂的同步问题可能不够灵活。

信号灯法的优点:

  1. 通用性强:信号灯是一种通用的同步机制,可以用于各种不同的同步问题,不仅限于生产者消费者模式。
  2. 可扩展性强:通过对信号灯的计数器进行适当的设置,可以控制线程的并发数量,适应不同的需求。
  3. 灵活性高:信号灯法提供了P操作和V操作,可以在任意位置进行信号量的操作,对于复杂的同步逻辑更加灵活。

信号灯法的缺点:

  1. 容易出错:使用信号灯需要手动编写P操作和V操作,容易出现错误,例如遗漏或错误的信号量操作。
  2. 可能产生死锁:使用信号灯时,需要谨慎设置信号量的初始值和操作顺序,否则可能导致死锁的发生。

(4)生产者消费者模式的优缺点:

优点:

  1. 并发性和吞吐量:生产者消费者模式允许生产者和消费者以不同的速度工作,从而提高系统的并发性和吞吐量。生产者和消费者可以并行执行,充分利用系统资源,提高系统的效率。
  2. 解耦和灵活性:生产者消费者模式将生产者和消费者解耦,使它们能够独立地演化和调整。生产者和消费者可以在不影响对方的情况下进行修改和优化,从而提高系统的灵活性和可维护性。
  3. 缓冲和异步处理:通过引入缓冲区,生产者消费者模式可以平衡生产者和消费者之间的速度差异。生产者可以在缓冲区中存储数据,而消费者可以异步地从缓冲区中取出数据进行处理,提供了更好的流量控制和数据处理能力。

缺点:

  1. 同步和竞态条件:生产者消费者模式需要合适的同步机制来确保生产者和消费者之间的正确协作。如果同步机制设计不当或实现有误,可能会导致竞态条件、死锁等问题的发生,增加了系统设计和调试的复杂性。
  2. 内存开销:由于需要引入缓冲区来存储数据,生产者消费者模式可能会增加系统的内存开销。缓冲区的大小需要合理设置,以保证系统性能和资源的有效利用,避免过大或过小的缓冲区带来的问题。
  3. 数据一致性:生产者消费者模式需要确保数据的一致性和正确性,特别是在多个生产者和消费者的情况下。需要仔细考虑数据的同步、顺序和处理逻辑,以避免数据丢失、重复或错误的情况发生。

综上所述,生产者消费者模式在提高并发性、解耦和灵活性方面具有明显的优势,但也需要合理考虑同步机制、内存开销和数据一致性等问题。在实际应用中,需要根据具体的需求和系统特点,综合考虑优缺点来选择是否采用生产者消费者模式。


11、什么是线程池?他有什么作用?如何实现?线程池大小如何确定?

线程池是一种用于管理和重用线程的机制。它由一组预先创建的线程组成,这些线程可以被反复使用来执行任务,而不需要频繁地创建和销毁线程。

线程池的作用:

  1. 提高性能和效率:线程池可以减少线程创建和销毁的开销,避免了频繁创建线程的系统开销。通过重用线程,可以降低线程创建和销毁的时间成本,提高系统的性能和效率。
  2. 提供资源管理和限制:线程池可以限制系统中同时运行的线程数量,从而控制系统的资源使用情况。通过设置线程池的大小和队列容量等参数,可以合理分配系统资源,避免资源耗尽的问题。
  3. 提供任务调度和处理:线程池可以接受并处理任务,通过任务队列和线程调度算法,可以合理地分配和执行任务。线程池可以根据系统的负载情况,自动调整线程数量和任务处理速度,提供灵活的任务调度和处理能力。

线程池的实现可以分为以下几个步骤:

  1. 创建线程池对象:创建一个线程池对象,包含线程池的属性和方法。
  2. 初始化线程池:在线程池对象中初始化线程池的大小、线程超时时间、任务队列等参数。
  3. 创建线程:根据线程池的大小,预先创建一定数量的线程,并将它们添加到线程池中。
  4. 接受任务:线程池提供一个任务队列,任务可以通过提交到任务队列中进行排队等待执行。
  5. 任务调度和执行:线程池中的线程从任务队列中获取任务,并执行任务的操作。线程池可以使用合适的调度算法来决定任务的执行顺序和线程的分配方式。
  6. 完成任务:线程执行完任务后,可以继续从任务队列中获取新的任务进行处理,直到线程池关闭或任务队列为空。
  7. 关闭线程池:当不再需要线程池时,可以调用关闭方法来停止线程池的运行,释放资源。

具体线程池的实现方式可以依赖于编程语言和平台的特性。在许多编程语言中,都提供了线程池的标准库或框架,可以直接使用或进行扩展实现。

这篇博客借鉴于B站狂神说,狂神真的很厉害,希望大家多多支持。

  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值