zoukankan      html  css  js  c++  java
  • 多线程总结与使用

    1. 什么是多线程

    1.1 什么是进程

    进程:每一个独立执行的程序,都可以称为一个进程,也就是"正在运行的程序"。

    1.2 什么是线程

    线程:我们知道每一个一个独立执行的程序就是一个进程,而在一个进程中还可以有多个执行单元同时运行,而这些执行单元就是线程。
    每一个进程中都至少存在一个线程。

    1.3 什么是多线程

    单线程:比如我们写一个main()方法,程序从开始到程序结束,整个过程只能顺序执行,如果某个地方出现问题,整个程序就会崩溃。
    多线程:指一个应用程序种有多个并发执行的线程,他们会交替执行,彼此间还可以进行通信。
    并发:指一个时间段,有多个线程交替执行。
    并行:指某个时刻,有多个线程同时执行
    特别注意:多线程并不是有多个线程同时执行,他是由CPU控制轮流执行的,知识CPU运行速度很快,像是同时执行的感觉。

    生活场景:
        单线程:售票大厅只开设一个售票窗口,所有人只能在这个窗口排队买票,虽然能实现买票功能,但是效率很低。
        多线程:售票大厅开设多个售票窗口,每个窗口都可以买票,大大提升效率。



    2. 多线程的创建

    1. 继承Thread
    2. 实现Runnable
    3. 实现Callable



    3. 线程的生命周期

    线程的生命周期包含5个阶段,包括:新建、就绪、运行、阻塞、销毁。

    1. 新建:就是刚使用new方法,new出来的线程;
    2. 就绪:就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;
    3. 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;
    4. 阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态;
    5. 销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;


    4. 线程的调度

    若某个时刻,某个线程就想被执行,这怎么办?
    在计算机中,线程的调度有两种模式:

    • 分时调度————————>让所有线程轮流获得CPU的调度
    • 抢占式调度————————>让所有线程抢夺CPU的使用权,优先级高的获取CPU调度的概率高

    Java采用的就是抢占式调度。

    4.1 线程的优先级

    线程的优先级用1~10之间的整数来表示,数字越大优先级越高。注意这句话,只是优先级高的越容易被调度而已。

    4.2 设置线程的优先级

    通过Thread类的setPriority(int newPriority)设置。



    5. 线程的休眠,等待,让步,插队

    上面说了线程优先级可以一定程度上把控线程的执行顺序,但如果你想人为地控制执行顺序,可以通过让线程休眠,等待,让步,插队来解决。



    5.1 线程休眠

    休眠:让当前正在执行的线程暂停一段时间,进入“阻塞状态”,多线程安全情况下,不会释放锁,导致其他线程无法执行。当休眠时间结束后,它会由“阻塞状态”变成“就绪状态”,从而等待cpu的调度执行。
    实现方式:调用线程的.sleep()方法,它会抛出InterruptedException异常。

    5.2 线程等待

    等待:让当前正在执行的线程等待一段时间,进入“阻塞状态”,只有等待其他线程的通知才能被唤醒。多线程安全情况下,会释放锁。

    5.3 线程让步

    让步:让当前正在执行的线程失去CPU的调度,让其回到就绪状态。让CPU重新调度一次,因为JAVA虚拟机采用抢占式调度,所以所有线程都会有再次抢占CPU的机会。
    实现方式:调用线程的.yield()方法。

    5.4 线程插队

    插队:若当前正在执行的线程调用了某个线程中join方法时,那么当前线程将会被阻塞,直到调用join方法的线程执行完之后,他才能继续执行。



    5.5 sleep()方法与wait()方法的区别与共同点

    • 两者最主要的区别在于:sleep ⽅法没有释放锁,⽽ wait ⽅法释放了锁 。

    • 两者都可以暂停线程的执⾏。Wait 通常被⽤于线程间交互/通信,sleep 通常被⽤于暂停执⾏。

    • wait() ⽅法被调⽤后,线程不会⾃动苏醒,需要别的线程调⽤同⼀个对象上的 notify() 或者notifyAll() ⽅法。

    • sleep() ⽅法执⾏完成后,线程会⾃动苏醒。或者可以使⽤ wait(longtimeout)超时后线程会⾃动苏醒。

    6. 多线程同步

    多线程的并发执行可以提高效率,但是当多个线程去访问同一个资源时,就会引发一些安全问题。
    实现方式:

    • 同步代码块————>锁对象是任意的,锁对象的创建不能放在run()方法里面,因为每个线程都会执行run方法,你放在里面,会导致每个线程创建一个锁对象。
    • 同步方法————>锁是当前调用该方法的对象,也就是this指向的对象。
    • 同步锁————>锁是由我们创建的,并且可以指定上锁和释放锁时机,而上述2个实现方式只有方法结束或发生异常时才释放锁,且有可能会出现死锁问题。

    看这里:https://blog.csdn.net/weixin_43921060/article/details/89681008



    7. 死锁

    概念:两个或多个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象就称为死锁。



    8. 线程间的通信

    场景假设:
          假设有生产者与消费者两个线程,他们同时去操作同一种商品,生产者负责生产商品,消费者负责消费商品。
          在保证线程同步后,该场景下,可能还会出现一个问题。就是生产者还没生产出商品,消费者就要去消费商品。

    线程通信:
          就是解决上述问题的,用于让线程间进行通信,保证线程任务的协调进行,在生产者线程产出商品后,调用wait()方法让该线程处于等待状态,等消费者线程执行消费完商品后,消费者调用notify()方法唤醒等待的生产者线程。
          为此,Java在Object类中提供了如下方法用于解决线程间的通信问题:

    • wait()————>让当前线程放弃锁进入等待【进入阻塞状态】,直到其他线程获得锁后,调用notify()或notifyAll()方法将其唤醒【从阻塞状态变为就绪状态】。
    • notify()————>唤醒所有之前处于过等待线程里面第一个调用wait()方法的线程
    • notifyAll()————>唤醒所有调用wait()方法的线程

    注意:

    1. 上述方法的使用必须在加锁条件下【我们需要先有一个Object对象,配合synchronize使用】,否则会报 IllegalMonitorStateException 异常。
    2. 当想要调用上述方法时,必须要取得这个锁对象的控制权(当前被Cpu调度),一般是放到synchronized(obj)代码中。


    9. 线程池

    线程池,是一种线程的使用方式,它为了降低线程使用中频繁的创建和销毁所带来的资源消耗与代价。
    通过创建一定数量的线程,让他们时刻准备就绪等待新任务的到达,而任务执行结束之后再重新回来继续待命。

    9.1 为什么要使用线程池

    不使用线程池的时候,当我们要使用多线程,就得去创建线程,用完释放销毁线程,每次用就得去创建,用完还得销毁。
    使用线程池的时候,让线程时刻准备就绪等待新任务的到达,而任务执行结束之后再重新回来继续待命。

    9.2 Java中线程池的使用

    1. 创建实现Runnable接口或Callable接口的实现类,同时重写方法

    2. 初始化Runnable接口或Callable接口的实现类

    3. 使用Executors线程执行其来创建线程池【得到ExecutorsService】【Executors是JDK5中增加的线程执行器工具类,提供了4种方法来创建用于不同需求的线程池】

    4. 使用ExecutorsService执行器的submit()方法将Runnable接口或Callable接口的实现类对象提交到线程池进行管理

    5. 线程任务执行完毕后,使用shutdown()方法关闭线程池

    9.3 如果提交任务时,线程池队列已满,这时会发生什么?

    如果一个任务不能被调度执行,那么ThreadPoolExcutor的submit()将会抛出一个RejectedExecutionException异常。


    Executors可以创建的线程池类型:

    1. ExecutorService newCachedThreadPool()
      说明:初始化一个可以缓存线程的线程池,默认缓存60s,线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列;
      特点:在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源;当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销;
      因此,使用时要注意控制并发的任务数,防止因创建大量的线程导致而降低性能。

    2. ExecutorService newFixedThreadPool(int nThreads)
      说明:初始化一个指定线程数的线程池,其中corePoolSize == maxiPoolSize,使用LinkedBlockingQuene作为阻塞队列
      特点:即使当线程池没有可执行任务时,也不会释放线程。

    3. ExecutorService newSingleThreadExecutor()
      说明:初始化只有一个线程的线程池,内部使用LinkedBlockingQueue作为阻塞队列。
      特点:保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行

    4. ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
      特定:初始化的线程池可以在指定的时间内周期性的执行所提交的任务,在实际的业务场景中可以使用该线程池定期的同步数据。

    看书382页
    


    10. 基于SpringBoot线程池实际开发中的应用

    需求:查看文章时,更新该文章的阅读数。
    利用线程池来优化提高效率。

    创建线程池配置类。
    @EnbleAsync:开启线程池功能
    
    创建Executor对象,并设置线程相关属性。
    

    说下这个方法哈,这里其实就是有个方法,用于去执行更新阅读数的操作。
    在该实现类中,用了线程池。
    
    @Async:使用线程池
    







    11. 基于SpringBoot线程池实际开发中的应用举例

    // 线程池配置类
    
    @Configuration
    @EnableAsync
    public class ThreadPoolConfig {
    	
    	@Bean("taskExecutor")
    	public Executor asyncServiceExecutor() {
    		ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    		// 设置核心线程数
    		executor.setCorePoolSize(5);
    		// 设置最大线程数
    		executor.setMaxPoolSize(20);
    		//配置队列大小
    		executor.setQueueCapacity(Integer.MAX_VALUE);
    		// 设置线程活跃时间(秒)
    		executor.setKeepAliveSeconds(60);
    		// 设置默认线程名称
    		executor.setThreadNamePrefix("异步线程");
    		// 等待所有任务结束后再关闭线程池
    		executor.setWaitForTasksToCompleteOnShutdown(true);
    		//执行初始化
    		executor.initialize();
    		return executor;
    	}
    }
    

    // 多线程执行任务类
    @Component
    public class ThreadServiceImpl  {
    	
    	@Async("taskExecutor")
    	public void print() {
    		try {
    			Thread.sleep(8000);
    			System.out.println("222222");
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    	}
    }
    

    	@GetMapping("/thread")
    	@ApiOperation("测试线程池")
    	public MsgResult threadTest() {
    		activityInfoService.threadTest(); // 花费5秒
    		
    		// 多线程执行
    		threadService.print(); // 花费8秒
    		
    		// 主线程执行完毕就会立即挽回   不会管——>多线程是否执行完毕
    		// 多线程:并不是同时执行  而是抢占式
    		return MsgResult.success("成功"); // 主线程执行完(5秒后,就会返回)  然后去执行多线程(8秒后执行完毕)
    	}
    
            // =========================================================
    
    	@GetMapping("/thread")
    	@ApiOperation("测试线程池")
    	public MsgResult threadTest() {
    		// 多线程执行
    		threadService.print(); // 花费8秒
    		
    		activityInfoService.threadTest(); // 花费5秒
    		
    		// 主线程执行完毕就会立即挽回   不会管——>多线程是否执行完毕
    		// 多线程:并不是同时执行  而是抢占式
    		return MsgResult.success("成功"); // 主线程执行完(5秒后,就会返回)  因为一开始就去执行了多线程  所以主线程执行完毕后  再过3秒多线程就执行完毕
    	}
    
  • 相关阅读:
    BASE64
    2020-2021-1 20201217《信息安全专业导论》第二周学习总结
    师生关系20201217
    2020-2021-1 20201217王菁<<信息安全专业导论>>第一周学习总结
    作业正文(快速浏览教材提出问题)
    自我介绍20201217王菁
    罗马数字转阿拉伯数字(20201225张晓平)
    20201225 张晓平《信息安全专业导论》第三周学习总结
    20201225 张晓平《信息安全专业导论》第二周学习总结
    我期待的师生关系
  • 原文地址:https://www.cnblogs.com/itlihao/p/14974443.html
Copyright © 2011-2022 走看看