线程的知识点和练习(全面)02

准备了通俗易懂的例子 希望能帮到大家

主要内容

线程

回顾线程01:

继承Thread类

实现Runnable接口

匿名内部类

callable接口

线程安全:

​ 同步代码块

​ 同步方法

​ 同步锁

​ 锁资源:this,类名.class

内容:

  • 能够说出线程的状态
  • 对线程的状态有一定的认识
  • 能够理解线程通信概念
  • 能够理解等待唤醒机制
  • 能够描述Java中线程池运行原理
  • 了解线程池
  • 能说出几种线程池
  • 简单使用线程池

一,线程的状态

1.1 线程状态概述

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,

有几种状态呢?在API中 java.lang.Thread.State 这个枚举中给出了六种线程状态:

线程状态导致状态发生条件
NEW(新建)线程刚被创建,但是并未启动。还没调用start方法。
Runnable(可运行)线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。
Blocked(锁阻塞)当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待)一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
Timed Waiting(计时等待)同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
Teminated(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

1.2 Runnable(运行状态)

Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

1.3 BLOCKED(锁阻塞)

Blocked状态在API中的介绍为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。 我们已经学完同步机制,那么这个状态是非常好理解的了。比如,线程A与线程B代码中使用同一锁,如果线程A获 取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。 这是由Runnable状态进入Blocked状态。除此Waiting以及Time Waiting状态也会在某种情况下进入阻塞状态。

Blocked 线程状态图

在这里插入图片描述

1.4 waiting(等待状态)

进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

1.5 Timed waiting(超时等待)

Timed Waiting在API中的描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。

  1. 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协

作关系。

  1. 为了让其他线程有机会执行,可以将Thread.sleep()的调用放到线程run()方法之内。这样才能保证该线程执行过程

中会睡眠 。

  1. sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。

在这里插入图片描述

二,线程通信(等待唤醒机制)重点

场景模拟:
*      顾客:告诉老板来份牛肉饺子,顾客进入等待状态
*      老板:花3s做好饺子,唤醒顾客,吃饺子

案例:创建顾客线程 类

//顾客:先继承thread类
// 告诉老板来份牛肉饺子,顾客进入等待状态
public class GuKe extends Thread {

    //将对象当成私有属性  一会儿保证两个线程使用同一个锁
    private Object object;

    public GuKe(Object object){
        this.object=object;
    }

    //线程操作 需要执行的代码
    @Override
    public void run() {
        while (true) {
            synchronized (object) {
                //1.告知老板
                System.out.println("老板,来份牛肉饺子");
                //2.自己进入等待状态

                try {
                    /*使用锁资源去调用wait等待方法 让顾客线程进入等待状态*/
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //唤醒顾客线程的代码执行后,顾客做的事情
                System.out.println(Thread.currentThread().getName()+"顾客开始吃饺子");
            }
        }
    }
}

创建老板线程类

/**
 * 老板线程  先继承thread类
 */
public class Boss extends Thread{

    //将对象当成私有属性  一会儿保证两个线程使用同一个锁
    private Object object;

    public Boss(Object object){
        this.object=object;
    }

    //老板:花3s做好饺子,唤醒顾客,吃饺子
    @Override
    public void run() {
        while (true){
            synchronized (object){
                try {
                    //花3s做好饺子 使用线程休眠模拟 做饺子的时间
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("饺子已经做好了");
                //做好饺子,唤醒顾客  使用同一个锁资源调用notify方法唤醒顾客线程。
                object.notify();
            }
        }
    }
}

测试类:

/**
 * 场景模拟:
 *      顾客:告诉老板来份牛肉饺子,顾客进入等待状态
 *      老板:花3s做好饺子,唤醒顾客,吃饺子
 *
 *      注意:
 *          老板和顾客都是线程类,都必须要继承Thread类
 *          要用同一个同步锁
 *          如果保证是同一个锁呢?
 *              将Objct对象当成参数传入 顾客和老板的线程类中去
 */
public class Demo02Test {

    public static void main(String[] args) {
        Object o = new Object();

        //创建并开启顾客线程
        new GuKe(o).start();

        //创建并开启顾客线程
        new GuKe(o).start();

        //创建并开启老板线程
        new Boss(o).start();
    }
}

如果是一次 单对单 代码没有问题,可以正常执行,但是只要一多,就出现错乱问题,生产者与消费者不能对应。

2.1 线程通信概述

概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。

为什么要处理线程间通信:

​ 多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

如何保证线程间通信有效利用资源:

​ 多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。

2.2 等待唤醒机制

什么是等待唤醒机制

​ 这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race,比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时 候你们更多是一起合作以完成某些任务。

​ 就是在一个线程进行了规定操作后,就进入等待状态wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。

wait/notify 就是线程间的一种协作机制。

等待唤醒中的方法

等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:

  1. wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify”在这个对象 上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中 。

  2. notify:则选取所通知对象的 wait 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。

  3. notifyAll:则释放所通知对象的 wait 上的全部线程。

注意:

​ 哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而 此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。

总结如下:

如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;

否则,线程就从 WAITING 状态又变成 BLOCKED 状态

调用waitnotify方法需要注意的细节

  1. wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。

  2. wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object的。

  3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法

2.3 生产者与消费者问题

等待唤醒机制其实就是经典的“生产者与消费者”的问题。等待唤醒机制如何有效利用资源。

流程:

​ 看有没有饺子

​ 如果没有饺子—》顾客线程等待----》老板线程做饺子----》唤醒顾客线程—》吃饺子。。。。。。

分析:

​ 需要的类:顾客线程类,老板线程类,资源类(饺子类)

​ 老板线程类:

​ 判断有没有饺子

​ 没有:生产饺子(3s生产时间),唤醒顾客线程,修改饺子的状态为true

​ 有:自己等待 , 唤醒顾客线程,

​ 顾客线程:

​ 判断有没有饺子:

​ 有:吃饺子,修改饺子的状态为false

​ 没有:自己等待,唤醒老板线程做饺子

​ 资源类(饺子)

​ boolean state = false;默认没有饺子

代码实现:

创建顾客线程:

public class GuKe extends Thread{

    private JiaoZi jiaoZi;

    public GuKe(JiaoZi jiaoZi){
        this.jiaoZi = jiaoZi;
    }

    @Override
    public void run() {
        while (true){
            System.out.println("顾客告知老板要饺子");
            synchronized (jiaoZi) {
                if (jiaoZi.isState()==false) {
                    try {
                        jiaoZi.wait();//顾客线程等待状态
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //唤醒顾客线程后执行的代码。
                System.out.println(Thread.currentThread().getName()+"顾客开始吃饺子");
                System.out.println("================================");
                jiaoZi.setState(false);//吃完饺子将状态变成false
                jiaoZi.notify();
            }
        }
    }
}

创建老板线程

public class Boss extends Thread{

    private JiaoZi jiaoZi;

    public Boss(JiaoZi jiaoZi){
        this.jiaoZi = jiaoZi;
    }

    @Override
    public void run() {
        while (true){
            synchronized (jiaoZi){
                //如果没有
                if(jiaoZi.isState()==true){
                    try {
                        jiaoZi.wait();//生产好饺子让老板等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }

                System.out.println("老板生产饺子");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                jiaoZi.setState(true);//将饺子的状态变成true饺子生产好了
                jiaoZi.notify();//如果有饺子 唤醒顾客线程吃饺子

            }
        }
    }
}

资源类:

public class JiaoZi {
    //定义状态 表示有没有饺子,默认没有饺子
    private boolean state = false;

    public boolean isState() {
        return state;
    }
    
    public void setState(boolean state) {
        this.state = state;
    }

    public JiaoZi() {
    }

    public JiaoZi(boolean state) {
        this.state = state;
    }
}

测试类:

public class DemoTest {

    public static void main(String[] args) {
        JiaoZi jiaoZi = new JiaoZi();

        //开启顾客线程
        new GuKe(jiaoZi).start();
        //开启老板线程
        new Boss(jiaoZi).start();
    }
}

三,线程池

3.1 为什么要使用线程池

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

在Java中可以通过线程池来达到这样的效果。

3.2 线程池概述

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

在这里插入图片描述

现在:每一次使用线程不在自己创建了,而是从线程池中取线程对象,用完后(调用close()方法)会将取出的线程对象还给线程池,下次如果需要在取。但是,如果在取线程对象时,线程池中没有空闲的线程对象,此时,该操作代码等到有空闲的线程对象了,再取用。

合理利用线程池能够带来的好处:

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机/宕机)。

3.3 线程池的使用

​ Java里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService 。 要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优 的,因此在 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。

3.3.1 newCachedThreadPool(缓存线程池)

特点:会根据需要创建新线程,如果线程池中有可复用的,会先复用可用的。

	 这个线程池典型的场景是改善 任务耗时短小的异步任务。

​ 使用execute可以复用可用的线程。如果没有可用线程,会创建新线程并添加到线程池里。

​ 那些1分钟没有被使用的线程将被停止并从缓存里移除。

public static void main(String[] args) {
    	//创建一个缓存线程池
        ExecutorService es = Executors.newCachedThreadPool();
        for(int i= 0;i<20;i++){
            final int index = i;
            //调用execute方法添加要执行的线程任务(操作)
            es.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"--->"+index);
                }
            });
        }
    }
3.3.2 newFixedThreadPool(int threadCount) 定容线程池

特点:

可以复用指定数目的线程

如果请求的线程数目大于目前idle的,那么多余的请求将被等待,直到线程池中有可用的线程。

如果有任何线程执行过程中停止了,将会新建一个线程代替。

线程池中的线程一直存活,直到显式的使用shutdown关闭。

public static void main(String[] args) {
        //创建一个定容线程池线程池
        ExecutorService es = Executors.newFixedThreadPool(10);
        for(int i= 0;i<200;i++){
            final int index = i;
            es.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"--->"+index);
                }
            });
        }
    }
3.3.3 newScheduledThreadPool(定时线程池)

特点:

支持定时及循环任务执行

延迟定时,延迟的时间间隔是从调用开始,开始计算的,并不受线程执行时间长短的影响

public static void main(String[] args) {
        ScheduledExecutorService se = Executors.newScheduledThreadPool(3);
        for(int i= 0;i<200;i++){
            final int index = i;
            se.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"--->"+index);
                }
            },5, TimeUnit.SECONDS);
        }
    }
3.3.4 newSingleThreadScheduledExecutor(单一线程池)

特点:最多只开启一个线程,对于队列中的Runnable,挨个执行

public static void main(String[] args) {
        ExecutorService es = Executors.newSingleThreadExecutor();
        for(int i= 0;i<200;i++){
            final int index = i;
            es.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"--->"+index);
                }
            });
        }
    }

线程池总结:

​ 缓存线程池: 有就复用 没有就创建 1分钟不用就干掉

​ 定容线程池: 有就复用 没有就等待复用

​ 定时线程池: 支持定时及循环任务执行

​ 单一线程池: 只有一个线程执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: CSP竞赛是中国青少年计算机应用能力大赛,旨在提高学生的计算思维能力和动手能力,培养创新精神和团队合作精神。CSP竞赛的知识点种类繁多,但以下几个关键知识点是必备的: 1. 数据结构:掌握栈、队列、链表、树等常用数据结构的基本原理和操作,能够熟练应用和选择合适的数据结构解决问题。 2. 算法设计与分析:了解常用的算法设计思想,如贪心算法、动态规划、分治法等,并能够分析算法的时间复杂度和空间复杂度。 3. 图论:熟悉图的基本概念和常见算法,如深度优先搜索、广度优先搜索、最短路径算法等,能够应用图论解决相关问题。 4. 数学基础:熟练掌握数论、组合数学、概率论等数学知识,能够利用数学方法解决问题。 5. 编程语言:具备良好的编程基础,掌握至少一种编程语言,如C++、Python等,能够熟练使用语言的基本语法和常见的数据结构与算法库。 6. 系统与网络知识:了解计算机系统的基本原理、操作系统的相关知识,并具备网络编程的基础知识。 以上是CSP竞赛中较为重要的知识点,掌握这些知识点将有助于解决CSP竞赛中的各类问题。参赛者还需要通过大量的练习和实践,提高编程能力和解题能力,才能在竞赛中取得好成绩。 ### 回答2: CSP竞赛是中国计算机学会举办的全国性高中生计算机竞赛,对于参赛选手来说,掌握一些必备的知识点是非常重要的。 首先,算法和数据结构是CSP竞赛的基础知识。选手需要掌握常见的排序算法(如冒泡排序、快速排序)、查找算法(如二分查找)和图算法(如最短路径算法、最小生成树算法)。此外,还需了解常见的数据结构,如数组、链表、栈、队列和树等。 其次,编程语言是参赛选手必备的工具。CSP竞赛使用C/C++和Pascal两种编程语言作为主力语言。选手需要熟悉这两种语言的语法和基本操作,掌握输入输出、变量和表达式等基本概念。 另外,数学基础知识也是CSP竞赛的重要组成部分。选手需要掌握数论、概率论和组合数学等基本概念,了解常见的数学运算和算法(如快速幂算法、最大公约数算法)等。 此外,选手还需熟悉计算机的基本原理和操作系统的基本知识,如二进制表示、计算机组成原理、进程和线程等。 最后,解题技巧和实践经验也是非常重要的。选手需要多做一些练习题和模拟赛,摸索出适合自己的解题方法,提升解题速度和准确性。 总之,CSP竞赛的必备知识点主要包括算法和数据结构、编程语言、数学基础、计算机原理和操作系统知识,以及解题技巧和实践经验。只有全面掌握这些知识,才能在竞赛中取得好的成绩。 ### 回答3: CSP竞赛是中国的计算机科学与技术竞赛,它对参赛者在算法设计与实现、数据结构、图论、动态规划等方面的知识要求较高。以下是CSP竞赛的必备知识点。 首先是算法设计与实现。参赛者需要了解各种基础算法,如贪心算法、分治算法、回溯算法、动态规划等,并能够熟练运用这些算法解决问题。此外,对于一些高级算法,如最大流、最小生成树、拓扑排序等,也需要有一定的了解。 其次是数据结构。CSP竞赛中经常需要用到的数据结构包括数组、链表、栈、队列、堆、树、图等。参赛者需要熟悉这些数据结构的特点、操作以及应用场景,能够灵活选择合适的数据结构解决问题。 图论也是CSP竞赛的必备知识点之一。参赛者需要了解图的基本概念,熟悉常用的图算法,如深度优先搜索、广度优先搜索、最短路径算法等。此外,对于一些高级的图论算法,如最小生成树算法、最大流算法、二分图匹配算法等,也需要有一定的了解。 动态规划是CSP竞赛中常用的解题方法。参赛者需要了解动态规划的基本原理、使用方法以及相关的优化技巧。能够通过推导状态转移方程、设计合适的状态表示和初始条件,从而优化问题的求解过程。 最后,参赛者还需要具备编程能力和解题思维。熟练掌握一门编程语言(如C++、Java等),能够使用编程语言实现算法,并能够分析问题、提炼问题的本质,找到解题的思路和方法。 以上是CSP竞赛的必备知识点,参赛者需要在这些方面进行深入学习和实践,以提高自己的竞赛水平。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值