多线程&线程池

多线程&线程池

1. 线程

  1. 线程的基本概念

    1. 进程:一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。进程一般由程序数据集合进程控制块三部分组成。程序用于描述进程要完成的功能,是控制进程执行的指令集;数据集合是程序在执行时所需要的数据和工作区;程序控制块包含进程的描述信息和控制信息是进程存在的唯一标志。

    2. 线程:线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID,当前指令指针PC,寄存器堆栈组成。而进程由内存空间(代码,数据,进程空间,打开的文件)和一个或多个线程组成。

      总结:

      1. 线程归属于进程,一个进程当中至少包含一个线程。
      2. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位。
      3. 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见。
      4. 调度和切换:线程上下文切换比进程上下文切换要快得多。
  2. 任务调度:

    1. 串行:多个任务,执行时一个执行完再执行另一个。
    2. 并行:每个线程分配各独立的核心,线程同时运行。
    3. 并发:多个线程在单个核心运行,同一时间一个线程运行,系统不停地切换线程,看起来像是同时运行,实际上是线程不停切换。

    并行与并发的区别

    ​ 在单CPU系统中,系统调度在某一时刻只能让一个线程运行,虽然这种调试机制有多种形式(大多数是时间片轮巡为主),但无论如何,要通过不断切换需要运行的线程让其运行的方式就叫并发(concurrent)。而在多CPU系统中,可以让两个以上的线程同时运行,这种可以同时让两个以上线程同时运行的方式叫做并行(parallel)。

    线程的调度:

    我们的CPU处理器有内核线程的概念,比如:
    单核单线程:代表计算机的CPU有一个核心内核,同一时间只能执行一个线程任务。

    4核8线程:代表计算机的CPU有4个核心内核,每个内核同一时间能执行两个线程任务。

    那如果我们在单核单线程的情况下,创建了多个线程的时候,CPU处理器该如何执行这些线程任务呢?

    通过JVM的线程调度机制!!!

    所有的Java虚拟机都有一个线程调度器,用来确定哪个时刻运行那个线程。

    线程调度器: 抢占式线程调度协调式线程调度

    在抢占模式下,操作系统负责分配CPU时间给各个进程,一旦当前的进程使用完分配给自己的CPU时间,操作系统将决定下一个占用CPU时间的是哪一个线程。因此操作系统将定期的中断当前正在执行的线程,将CPU分配给在等待队列的下一个线程。所以任何一个线程都不能独占CPU。每个线程占用CPU的时间取决于进程和操作系统。进程分配给每个线程的时间很短,以至于我们感觉所有的线程是同时执行的。实际上,系统运行每个进程的时间有2毫秒,然后调度其他的线程。它同时他维持着所有的线程和循环,分配很少量的CPU时间给线程。线程的的切换和调度是如此之快,以至于感觉是所有的线程是同步执行的。

  3. 创建线程的方式:

    1. 继承Thread类

      public class Thread02_create01 {
          public static void main(String[] args) {
              // 创建线程对象
              MyThread myThread = new MyThread();
              // 启动线程
              myThread.start();
          }
          // 继承Thread类  实现run方法
          static class MyThread extends Thread{
              @Override
              public void run() {
                  for (int i = 0; i < 1000; i++) {
                      System.out.println("输出打印"+i);
                  }
              }
          }
      }
      
    2. 匿名内部类

      public static void main(String[] args) {
              //使用匿名内部类方式创建Runnable实例
              Thread t1 = new Thread(new Runnable(){
                  @Override
                  public void run() {
                      for (int i = 0; i < 1000; i++) {
                          System.out.println("输出"+i);
                      }
                  }
              });
              t1.start();
              // lambda 表达式简化语法
              Thread t2 = new Thread(()->{
                  for (int i = 0; i < 1000; i++) {
                      System.out.println("输出"+i);
                  }
              });
              t2.start();
      }	
      
    3. 实现Runnable接口

      public class Thread03_create02 {
          public static void main(String[] args) {
              // 创建线程对象   传入要执行的任务
              Thread thread = new Thread(()->{
                  // do something
              });
              // 调用线程.start方法
              thread.start();
          }
          // 实现Runnable接口  实现run方法
          static class MyRunnable implements Runnable {
              @Override
              public void run() {
                  for (int i = 0; i < 1000; i++) {
                      System.out.println("输出:"+i);
                  }
              }
          }
      }
      
    4. 实现Callable接口

      public class Thread03_create03 {
          public static void main(String[] args) {
              //FutureTask包装我们的任务,FutureTask可以用于获取执行结果
              FutureTask<Integer> ft = new FutureTask<>(new MyCallable());
      
              //创建线程执行线程任务
              Thread thread = new Thread(ft);
              thread.start();
              try {
                  //得到线程的执行结果
                  Integer num = ft.get();
                  System.out.println("得到线程处理结果:" + num);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              } catch (ExecutionException e) {
                  e.printStackTrace();
              }
          }
          // 实现Callable接口,实现带返回值的任务
          static class MyCallable implements Callable<Integer> {
              @Override
              public Integer call() throws Exception {
                  int num = 0;
                  for (int i = 0; i < 1000; i++) {
                      System.out.println("输出"+i);
                      num += i;
                  }
                  return num;
              }
          }
      }
      

      区别:实现Callable接口创建线程的方式可以获取到返回值,其他的不行。

  4. 用户线程和守护线程:通过Thread.setDaemon(false)设置为用户线程;通过Thread.setDaemon(true)设置为守护线程。如果不设置次属性,默认为用户线程。

    两者其实基本上一样,唯一的区别在于JVM何时离开。

    1. 用户线程:主线程结束后用户线程还会继续运行,JVM存活,平时用到的普通线程均是用户线程,当在Java程序中创建一个线程,它就被称为用户线程。
    2. 守护线程:如果没有用户线程,都是守护线程,那么JVM结束。守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。

  5. 多线程的运行状态和生命周期

    1. 线程的状态

      1. 新建状态 (NEW)
        当用new操作符创建一个线程时, 例如new Thread®,线程还没有开始运行,此时线程处在新建状态。 当一个线程处于新生状态时,程序还没有开始运行线程中的代码
      2. 就绪状态
        一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态
        处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由运行时系统的线程调度程序(thread scheduler)来调度的。
      3. 运行状态 (RUNNABLE)
        当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法。
      4. 阻塞状态 (WAITING)(TIMED_WAITING)(BLOCKED)
        线程运行过程中,可能由于各种原因进入阻塞状态:
        1>线程通过调用sleep方法进入睡眠状态;
        2>线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
        3>线程试图得到一个锁,而该锁正被其他线程持有;
        4>线程在等待某个触发条件;
      5. 死亡状态 (TERMINATED)
        有两个原因会导致线程死亡:
        1) run方法正常退出而自然死亡,
        2) 一个未捕获的异常终止了run方法而使线程猝死。
        为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false.
    2. 线程的生命周期
      在这里插入图片描述

  6. 线程的优先级

    ​ 线程的切换是通过线程调度来控制的,我们无法通过代码来干预,但是我们通过提高线程的优先级来最大程度的改善线程获取时间片的几率。

    ​ 线程优先级由低到高分为1-10,有三个常量来表示:

    Thread.MIN_PRIORITY 1
    Thread.MAX_PRIORITY 10
    Thread.NORM_PRIORITY 5
    void setPriority(int priority):设置线程的优先级
    
  7. Java并发编程三大特性 && 并发编程之线程安全

    1. JVM模型

      Java内存模型(即Java Memory Model,简称JVM)。

      ​ JVM本身是一种抽象的概念,并不真实存在,他描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(即栈空间),用于存储线程私有的数据。而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问

      ​ 线程对变量的操作(读取赋值等)都必须在工作内存中操作,首先要将变量从主内存中的变量副本拷贝,不能直接操作主内存中的变量,工作内存中存储着主内存中变量的副本拷贝,而每个工作内存都是每个线程私有的,线程间的通信(传值)必须通过主内存来完成,如下图:

在这里插入图片描述

  1. Java并发编程三大特性

    1. 原子性:共享变量的修改,是在工作内存中修改同一份共享变量的不同副本,然后多个线程的修改回填入主内存中时就会有覆盖产生,导致原子性问题。

    2. 可见性:当有线程修改了主内存的值的时候,其他线程还是一直复用之前读取到的副本的变量值,就会导致可见性问题。

    3. 有序性:JVM指定重排序。

      volatile:解决可见性和有序性问题,屏蔽指令重排序。

      1. 保证变量的可见性:当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。当一个线程向被volatile关键字修饰的变量写入线程的时候,虚拟机会强制它被值刷新到主内存中。当一个线程用到被volatile关键字修饰的值的时候,虚拟机会强制它从主内存中读取。
      2. 屏蔽指令重排序:指令重排序是编译器和处理器为了高效对程序进行优化的手段,他只能保证程序执行的结果是正确的,但是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题。
      

      synchronized:关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见,即可以替代volatile。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bDanwEEr-1624728805228)(asset\syncronized.png)]

      注意:对象如同锁,持有锁的线程可以在同步中执行,没持有锁的线程即使获取到cpu的执行权,也进不去。

      同步的前提:

      1. 必须要有两个或两个以上的线程。
      2. 必须是多个线程使用同一个锁。

      优缺点:

      ​ 好处:解决了多线程的安全问题

      ​ 弊端:多个线程需要判断锁,较为消耗资源,抢锁的资源。

      修饰方法注意点:

      1. synchronized修饰方法使用锁是当前this锁
      2. synchronized修饰静态方法使用锁是当前的字节码文件。
      3. 多个线程保证同步必须使用同一个锁。

      lock锁: Lock lock = new ReentrantLock();

      lock与synchronized的区别:

      1)Lock是一个接口,而synchronized是Java中的关键字 
      	synchronized是内置的语言实现;
      	synchronized关键字可以直接修饰方法,也可以修饰代码块,而 lock只能修饰代码块
      2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;
        Lock在发生异常时,如果没有主动通过unLockO去释放锁,则很可能造成死锁现象,因此使用Lock时需要在fina11y块中释放锁;
      
      3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用 synchronized时,等待的线程会一直等待下去,不能够响应中断;
      
      4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。(提供tryLock)
      
      5)Lock可以提高多个线程进行读操作的效率。(提供读写锁)
      
      在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优干 synchronized。所以说,在具体使用时要根据适当情况选择
      

2. 线程池

  1. 线程池基础

    1. 什么是线程池?

      ​ 线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。这里的线程就是我们前面学过的线程,这里的任务就是我们前面学过的实现了Runnable或Callable接口的实例对象;

    2. 为什么使用线程池?

      ​ 使用线程池最大的原因就是可以根据系统的需求和硬件环境灵活的控制线程的数量,且可以对**所有线程进行统一的管理和控制,**从而提高系统的运行效率,降低系统运行运行压力;当然了,使用线程池的原因不仅仅只有这些,我们可以从线程池自身的优点上来进一步了解线程池的好处;

    3. 线程池的优势:

      1. 线程和任务分离,提升线程重用性
      2. 控制线程并发数量,降低服务器压力,统一管理所有线程。
      3. 提升系统响应速度,假如创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了T1和T3的时间。
    4. 线程池的体系结构

      java.util.concurrent.Executor   // 线程池的顶级接口定义了线程池的最基本方法
      	java.util.concurrent.ExecutorService    // 定义常用方法
      		java.util.concurrent.ThreadPoolExecutor   // 线程池的核心实现类
      			java.util.concurrent.ScheduledThreadPoolExecutor  
      java.util.concurrent.Executors  // 线程池的工具类 			
      
      关键类或接口含义
      Executor是一个接口,它是Executor框架的基础,
      它将任务的提交与任务的执行分离开来
      ExecutorService线程池的主要接口,是Executor的子接口
      ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务
      ScheduledThreadPoolExecutor另一个关键实现类,可以进行延迟或者定期执行任务。ScheduledThreadPoolExecutor比Timer定时器更灵活,
      功能更强大
      Future接口与FutureTask实现类代表异步计算的结果
      Runnable接口和Callable接口的实现类都可以被ThreadPoolExecutor或
      ScheduledThreadPoolExecutor执行的任务
      Executors(阿里巴巴规范禁止)线程池的工具类,可以快捷的创建线程池
      1. Executor

        线程池就是线程的集合,线程池集中管理线程,以实现线程的重用,降低资源消耗,提高响应速度等。线程用于执行异步任务,单个的线程既是工作单元也是执行机制,从JDK1.5开始,为了把工作单元与执行机制分离开,Executor框架诞生了,它是一个用于统一创建任务与运行任务的接口。框架就是异步执行任务的线程池框架。
        
      2. ThreadPoolExecutor

        public ThreadPoolExecutor(
        	int corePoolSize, //核心线程数量
            int maximumPoolSize,// 最大线程数
            long keepAliveTime, // 最大空闲时间(非核心线程无任务的存活时间)
            TimeUnit unit,         // 时间单位
            BlockingQueue<Runnable> workQueue,   //  任务队列
            ThreadFactory threadFactory,    // 线程工厂
            RejectedExecutionHandler handler  //  饱和处理机制
        ) 
        
        
        1. 线程池的三种队列

          1. SynchronousQueue:无缓冲等待队列,不存储元素

            SynchronousQueue没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。
            使用SynchronousQueue阻塞队列一般要求maximumPoolSizes为无界,避免线程拒绝执行操作。
            
          2. LinkedBlockingQueue:无界缓存等待队列

            LinkedBlockingQueue是一个无界缓存等待队列。
            当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待。(所以在使用此阻塞队列时maximumPoolSizes就相当于无效了),
            每个线程完全独立于其他线程。
            生产者和消费者使用独立的锁来控制数据的同步,即在高并发的情况下可以并行操作队列中的数据。
            
          3. ArrayBlockingQueue:有界缓存等待队列

            ArrayBlockingQueue是一个有界缓存等待队列,可以指定缓存队列的大小,当正在执行的线程数等于corePoolSize时,多余的元素缓存在ArrayBlockingQueue队列中等待有空闲的线程时继续执行,当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败,会开启新的线程去执行,当线程数已经达到最大的maximumPoolSizes时,再有新的元素尝试加入ArrayBlockingQueue时会报错。
            
        2. 饱和处理机制

          1. AbortPolicy:饱和时候抛出异常,丢弃任务
          2. CallerRunsPolicy:调节机制,将任务退回给调用者,让调用者帮忙执行。
          3. DiscardPolicy:丢弃新加入的任务。
          4. DiscardOldestPolicy:将队列中最老的任务丢弃,尝试提交新任务。
      3. Execurot线程工具类

        // Executors是线程池的工具类,提供了四种快捷创建线程池的方法:
        
        newCachedThreadPool // 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
        
        newFixedThreadPool // 创建一个定长线程池,传入线程最大并发数,可控制线程最大并发数,超出的线程会在队列中等待。
        
        newSingleThreadExecutor  // 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
        
        newScheduledThreadPool // 创建一个定长线程池,支持定时及周期性任务执行。
        
      4. 线程池执行流程
        在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值