多线程&JUC(二)

目录

一、等待唤醒机制

1.生产者消费者

等待唤醒机制可以简单的理解为下图。厨师相当于生产者,吃货相当于消费者。当桌子(缓冲区)上没有食物时,吃货等待,厨师抢夺到CPU使用权生产食物,然后将食物放到桌子上,再唤醒吃货。因为此时桌子上有食物,所以厨师等待,吃货拿到CPU使用权开始吃,吃完之后唤醒厨师继续做,自己等待。


代码实现如下:

java 复制代码
public class Desk {
    /*
        控制生产者和消费者的执行
     */

    //判断桌子上是否有食物
    public static int foodFlag = 0;

    //消费者能吃的食物总数
    public static int count = 10;

    //生产这能做的面条总数
    public static int noodle = 10;

    //锁对象
    public static final Object obj = new Object();
}
java 复制代码
public class Foodie extends Thread{
    @Override
    public void run() {

       while (true) {
           synchronized (Desk.obj) {
               if (Desk.count == 0) {
                   break;
               }else {
                   //先判断桌子上是否有面条
                   if (Desk.foodFlag == 0) {
                       //如果没有就等待
                       try {
                           Desk.obj.wait();  //当前线程跟锁进行绑定
                       } catch (InterruptedException e) {
                           throw new RuntimeException(e);
                       }
                   }else {
                       try {
                           Thread.sleep(1000);
                       } catch (InterruptedException e) {
                           throw new RuntimeException(e);
                       }
                       Desk.count--;
                       System.out.println("吃货在吃面条, 还能再吃" + Desk.count + "碗面条");
                       Desk.obj.notifyAll();//唤醒所有绑定在这个锁对象上的线程
                       Desk.foodFlag = 0;
                   }
               }
           }
       }
    }
java 复制代码
public class Cook extends Thread{
    @Override
    public void run() {
        while (true) {
            synchronized (Desk.obj) {
                if (Desk.noodle == 0) {
                    break;
                }else {
                    if (Desk.foodFlag == 1) {
                        try {
                            Desk.obj.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }else {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                        Desk.noodle--;
                        System.out.println("厨师正在做面条,还能再做" + Desk.noodle +"碗面条");
                        Desk.obj.notifyAll();
                        Desk.foodFlag = 1;
                    }
                }
            }
        }
    }
}
java 复制代码
public class WNDemo1 {
    public static void main(String[] args) {
        /*
         *    需求:完成生产者和消费者(等待唤醒机制)的代码
         *          完成线程轮流交替执行的效果
         */

        Foodie f = new Foodie();
        Cook c = new Cook();

        f.start();
        c.start();

    }
}

运行结果的部分截图:

2.阻塞队列

阻塞队列可以理解为消费者和生产者之间的管道,生产者往队列中放数据,消费者从队列中拿数据。

代码如下:

java 复制代码
public class Cook extends Thread{
    private ArrayBlockingQueue<String> queue;

	//测试类中定义阻塞队列,然后传给生产者和消费者进程,保证两个进程用的是同一个队列。
    public Cook(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            //不断的往阻塞队列中华放面条
            try {
                //put方法的底层就已经给线程上锁
                queue.put("面条");
                System.out.println("厨师放了一碗面条");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
java 复制代码
public class Foodie extends Thread{

    private ArrayBlockingQueue<String> queue;

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


    @Override
    public void run() {
        while (true) {
            //从阻塞队列中取出数据
            try {
                queue.take();
                System.out.println("吃货吃了一碗面条");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
java 复制代码
public class WNDemo2 {
    public static void main(String[] args) throws InterruptedException {
        /*
            利用阻塞队列完成生产者和消费者
            细节:
                生产者和消费者必须使用同一个阻塞队列
         */

        //创建阻塞队列的对象
        //参数传递的是阻塞队列的大小
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);

        Cook c = new Cook(queue);
        Foodie f = new Foodie(queue);

        c.start();
        f.start();

    }
}

可以发现运行结果中,吃货和厨师的打印语句重复打印了。是因为take和put方法底层就已经给线程上锁了,但是打印语句并没有在锁的内部,所以导致打印语句重复打印。

3.线程的状态

线程有七种状态,如下图所示:

但是要注意在JVM(Java虚拟机)中是没有定义运行态的,因为当一个线程抢到CPU的使用权之后,JVM就会把当前的线程交给操作系统去管理,JVM就不管了,所以就没有定义运行态。

所以Java中的线程就只有以下六种状态:

二、线程池

1.理解与使用

这里先来讲个小故事,假设A同学中午要吃饭,但是他没有碗,然后他就去买了一个碗,吃完饭之后就把碗给砸了。到了晚上,又要吃饭,但是发现又没有碗,然后又去买了一个碗,吃完饭后又把碗给砸了。是不是既浪费时间又浪费资源,上述创建线程跟这个是差不多的,当我们用到线程的时候就创建,用完之后线程就消失,浪费了操作系统的资源。

因此,为了解决这个问题,就需要准备了容器用来存放线程,这个容器就叫线程池 。刚开始线程池里面是空的,当我们给线程池去提交一个任务的时候,线程池本身就会自动地去创建一个线程,拿着这个线程就去执行任务,执行完了就把线程还回线程池中。当第二次再提交一个任务的时候,就不需要创建新的线程了,拿着已经存在的线程去执行任务,执行完后再还回去。但是还有一种特殊情况,当线程1还在执行第一个任务的时候,提交了第二个任务,此时线程池里会再新建一个线程去执行任务2

当线程池有容量限制时,情况就如下图所示,后面的任务只能等线程执行完前面的任务之后再去执行它们。

主要核心原理:

线程池代码实现

利用Executors类创建一个线程池对象。

当向线程池中提交5个任务时,结果是什么样呢?

java 复制代码
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {

            System.out.println(Thread.currentThread().getName() + "-----" + i);
        }
    }
}
java 复制代码
public class ThreadPool {
    public static void main(String[] args) {

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

        MyRunnable mr = new MyRunnable();

        //2.提交线程池的任务
        pool1.submit(mr);
        pool1.submit(mr);
        pool1.submit(mr);
        pool1.submit(mr);
        pool1.submit(mr);
    }
}

我们会发现线程池中创建了5个线程,

线程复用演示如下:

java 复制代码
public class MyRunnable implements Runnable{
    @Override
    public void run() {
       System.out.println(Thread.currentThread().getName() + "-----" + 1);
    }
}
java 复制代码
public class ThreadPool {
    public static void main(String[] args) throws InterruptedException {

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

        MyRunnable mr = new MyRunnable();

        //2.提交线程池的任务
        pool1.submit(mr);
        //让main线程睡1s是要让任务执行完毕赶紧把线程还回去
        Thread.sleep(1000);
        pool1.submit(mr);
        Thread.sleep(1000);
        pool1.submit(mr);
        Thread.sleep(1000);
        pool1.submit(mr);
        Thread.sleep(1000);
        pool1.submit(mr);
    }
}

发现5个任务都是线程1来执行的。

2.自定义线程池

这里还是要先讲一个小故事,A同学开了一家饭店,这个饭店服务员对顾客是一对一服务的,当顾客走了,这个服务员才能去服务别人。这个饭店里有6名服务员,3位正式员工,3位临时员工。当临时员工一段时间空闲下来,为了解决成本,老板就需要把临时员工给辞退,但是正式员工是不能辞退的。如果生意特别好,有许多人在门外排队,但是老板最多只允许10名顾客排队,其他的等明天再来。

上述的故事就跟自定义线程池中的参数是相似的。

当自定义线程池中有3条核心线程、3条临时线程时,有以下几种情况。

情况一(提交三个任务)

线程池就会创建三条线程去处理这三个任务。

情况二(提交五个任务)

线程池会先创建三个线程来处理任务,剩下的两个线程排队等待,直到有了空闲的线程才去执行剩下的任务。

情况三(提交8个任务)

线程池会先创建三个线程来处理任务,剩余的任务就会在后面排队,现在定义队伍的长度为3,所以只有三个任务(任务4 5 6)在队列中排队等待。这个时候,线程池才会创建临时线程去处理任务7和任务8。由此可见,先提交的任务不一定会先执行

即核心线程没有空闲且队伍已经排满了,才会去创建临时线程

情况四(提交10个任务)

此时,提交的任务数量太多,已经超过了核心线程数量 + 临时线程数量 + 队伍长度。线程池会先创建三个核心线程来执行任务1 2 3, 然后任务4 5 6去队列中等待,线程池再创建三个临时线程去执行任务7 8 9,任务10就会触发任务拒绝策略,直接舍弃不要。

任务拒绝策略有以下的几种,常用的是第一个:

代码实现如下:

java 复制代码
public class ThreadPool2 {
    public static void main(String[] args) {
        /*
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor
            (核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);

            参数一:核心线程数量                 不能小于0
            参数二:最大线程数                  不能小于等于0,最大数量>=核心线程数量
            参数三:空闲线程最大存活时间          不能小于0
            参数四:时间单位                    用TimeUnit指定
            参数五:任务队列                    不能为null
            参数六:创建线程工厂                 不能为null
            参数七:任务的拒绝策略                不能为null
         */

        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3,
                6,
                60,
                TimeUnit.SECONDS,   //秒
                new ArrayBlockingQueue<>(3),  //任务队列就使用阻塞队列
                Executors.defaultThreadFactory(),  //线程池获取线程,这个方法在底层也是new了一个Thread
                new ThreadPoolExecutor.AbortPolicy() // 任务的拒绝策略(AbortPolicy是一个静态内部类)
        );


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

    }
}

三、线程池额外知识

线程池设置多大合适呢??

首先要看电脑的最大并行数,有的电脑是4核8线程,有的电脑是8核16线程等等。线程数就是最大并行数。

CPU密集型指的是程序中运算步骤比较多,IO密集型指的是文件操作多的程序

相关推荐
gyc27277 分钟前
快速熟悉JavaScript
开发语言·前端·javascript
越甲八千11 分钟前
C++海康相机DEMO
开发语言·c++·数码相机
0白露38 分钟前
PHP之特性
开发语言·php
曹天骄43 分钟前
Spring Boot Gradle 项目中使用 @Slf4j 注解
java·spring boot·后端
青春_strive1 小时前
Qt:事件
开发语言·qt
虾球xz1 小时前
游戏引擎学习第133天
java·学习·游戏引擎
吉星9527ABC1 小时前
Linux下的c进程和java进程的通信-UnixSocket
java·linux·c语言·unixsocket通讯
浪九天1 小时前
Java常用正则表达式(身份证号、邮箱、手机号)格式校验
java·开发语言·正则表达式
初级代码游戏1 小时前
MAUI(C#)安卓开发起步
android·开发语言·c#·hyper-v·maui·haxm·aehd
赔罪1 小时前
Python 面向对象高级编程-定制类
服务器·开发语言·前端·python