Java八股文整理Day3-Java多线程

本文详细介绍了Java多线程的基础知识,包括程序、进程、线程的概念,以及并行、并发的区别。接着讨论了多线程的优点和应用场景,并列举了线程的四种创建方式:继承Thread、实现Runnable、Callable接口和线程池。文章还分析了线程的生命周期、状态转换,以及线程的常用方法。此外,探讨了Java的四种线程池和拒绝策略,并对比了ThreadLocal和Synchronized的区别。

Java多线程

个人结合所学知识整理,如有错误理解敬请指正,感谢🙏

0.基础知识

程序、进程、线程
  • 程序(program)本质上是为了完成某项特定任务而使用某种语言编写的指令集合
  • 进程(process)是程序的一次执行过程,当程序开始运行,会被加载到内存中,执行时占用cpu资源,执行完毕释放资源(产生-存在-消亡的过程即线程的生命周期);进程可以理解为cpu的资源分配单位,系统在运行时会给每个进程分配不同的内存区域
  • 线程(thread)进程的进一步细化,是程序的执行路径。如果一个进程能够同步执行多个线程,那么这个进程就是支持多线程的;线程可以理解为cpu调度和执行单位,每个线程拥有独立的运行栈和程序计数器,线程切换开销小
并行、并发
  • 并行:多个cpu同时执行多个任务,如多个人做不同的事,但实际上此时一个人只能做一件事
  • 并发:一个cpu同时做多个任务,如一个人同时做多个任务,但实际上同一时间并不是做个任务同时在做,而是采用时间片的技术,分时执行
多线程的优点
  • 提高程序的响应速度,提高用户的使用体验
  • 提高cpu利用率,减少资源闲置
  • 改善程序结构,将复杂的程序细化为多个线程独立运行,有利于理解和修改
多线程的应用场景
  • 程序需要同时执行多个任务
  • 避免程序在执行==需要等待的任务(如用户输入、文件读写操作、网络操作、搜索等)==时阻塞其他任务的运行
  • 需要一些后台执行的程序是也需要用到多线程
Java线程的分类
  • 用户线程:开发者自己编写的线程
  • 后台(守护)线程:用来服务用户线程的线程

1.线程的创建

线程的四种创建方式
  1. 继承Thread类

    //1.创建线程类并继承Thread类,重写run方法
    public class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("myThreadRun");
        }
    }
    //测试
    @Test
    void testCreateThreadByExtends(){
      //创建线程实例
      MyThread myThread = new MyThread();
      //调用start方法运行线程
      myThread.start();
    }
    //运行结果:myThreadRun
    
  2. 实现Runnable接口(线程无返回值)

    //2.实现Runnable接口,重写run方法
    public class MyThread implements Runnable {
        @Override
        public void run() {
            System.out.println("myThreadRun");
        }
    }
    //测试
    @Test
    void testCreateThreadByImplements(){
      //创建线程实例
      MyThread myThread = new MyThread();
      //实例化Thread类,并传入自己的线程类
      Thread thread = new Thread(myThread);
      //调用start方法运行线程
      thread.start();
    }
    //运行结果:myThreadRun
    
  3. 实现Callable接口(线程有返回值)

    //3.实现Callable接口,泛型为返回值类型,重写call方法
    public class MyCallableThread implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            int sum = 0;
            for (int i = 1; i < 10; i++) {
                if(i%2==0){
                    sum += i;
                }
            }
            return sum;
        }
    }
    //测试
    @Test
    void testCreateCallableThread(){
      //创建自定义线程
      MyCallableThread callableThread = new MyCallableThread();
      //创建FutureTask实例并传入自定义线程
      FutureTask<Integer> futureTask = new FutureTask<>(callableThread);
      //创建线程实例并传入FutureTask实例
      Thread thread = new Thread(futureTask);
      //调用start方法启动线程
      thread.start();
      //获取线程返回的值
      try {
        Integer sum = futureTask.get();
        System.out.println("10以内偶数和为:" + sum);
      } catch (InterruptedException e) {
        e.printStackTrace();
      } catch (ExecutionException e) {
        e.printStackTrace();
      }
    }
    //输出结果:10以内偶数和为:20
    
  4. 通过线程池创建

    //4.创建自定义线程,通过线程池执行线程
    public class MyThread implements Runnable {
        @Override
        public void run() {
                System.out.println("MyThreadRun");
        }
    }
    //测试
    @Test
    void testThreadPool(){
      //创建指定数量的线程池
      ExecutorService  service = Executors.newFixedThreadPool(10);
      ThreadPoolExecutor threadPool = (ThreadPoolExecutor) service;
      //执行指定的线程
      threadPool.execute(new MyThread());//适用于实现Runnable接口的线程
      //service.submit(CallableThread) 适用于实现Callable接口的线程
      //关闭线程池
      threadPool.shutdown();
    }
    //输出结果:MyThreadRun
    

2.四种创建线程方式的比较

Thread类和Runnable接口
  • 继承Thread类和实现Runnable接口两种方式都需要重写run()方法,线程执行逻辑都在这个方法中

  • Thread类实际上也实现了Runnable接口

  • Java仅支持单继承,因此如果一个类已经继承了有父类,那么就不能够再继承Thread类,但Java支持同时实现多个接口,因此使用实现Runnable接口创建线程类的方式更加实用

  • Runnable接口实现线程更有益于多线程数据共享,只需要实例化一个线程对象,将这个对象传入Thread的构造器中,多个线程之间的变量不需要使用static关键字,即可达到共享变量的效果

    public class MyThread implements Runnable {
        int num;
        public MyThread(int num) {
            this.num = num;
        }
        @Override
        public void run() {
                System.out.println(this.num);
        }
    }
    //测试
    @Test
    void testRunnableThread(){
      //只需要实例化一个自定义线程
      MyThread myThread = new MyThread(3);
      //传入到不同的Thread实例中即可达到共有变量的效果
      Thread thread1 = new Thread(myThread);
      Thread thread2 = new Thread(myThread);
      thread1.start();//输出3
      thread2.start();//输出3
    }
    
  • 继承Thread的方式需要实现多个Thread子类来创建多个线程,如果线程类中的变量不使用static关键字将其变为类级别变量,那么每个实例对象都会有自己独立的变量,无法共享

    public class MyThread extends Thread {
        int num;
        static String share;
        public MyThread(int num) {
            this.num = num;
        }
        public MyThread(int num,String share) {
            this.num = num;
            this.share = share;
        }
        @Override
        public void run() {
                System.out.println("num=" + this.num + ",share=" +this.share);
        }
    }
    //测试
    @Test
    void testThread(){
      //需要实例化多个对象创建多个线程
      MyThread thread1 = new MyThread(3, "static变量");
      MyThread thread2 = new MyThread(1);
      thread1.start();//输出:num=3,share=static变量
      thread2.start();//输出:num=1,share=static变量
    }
    
Callable接口
@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     * 
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}
  • 相比前两种方式执行逻辑需要写在无返回值的run()方法内,实现Callable接口的线程类执行逻辑放在了支持泛型返回值的call()方法内,且可以抛出异常被外部的操作捕获

  • 但这种方式比前两种稍微麻烦,需要借助FutureTask类,如获取返回值,需要在线程启动计算之后借助FutureTask类的get()方法

线程池
  • 通过线程池可以提前创建好一定数量的线程,需要使用时直接取出,避免频繁地创建和销毁线程,实现线程重复利用
  • 好处是可以减少创建线程的时间,提高响应速度,避免高并发下经常创建和销毁线程,节省资源消耗,且利于管理线程

3.线程的生命周期

生命周期
  • 线程的生命周期值线程从创建到销毁经历的过程
  • 一般而言,线程有==新建(new)、就绪(runnable)、运行(running)、阻塞(blocked)、死亡(dead)==五种状态
    1. 新建:当使用new关键字实例化一个线程对象后,该线程就处于新建状态,此时由JVM给其分配内存并初始化成员变量的值
    2. 就绪:当实例对象调用了start()方法,线程就处于就绪状态,JVM为其创建方法调用栈和程序计数器,等待调用
    3. 运行:当处于就绪状态的线程获得了cup的使用权,就开始run()方法中的方法体,此时线程处于运行状态
    4. 阻塞:当运行中的线程被某种原因让出了cpu的使用权而暂时停止运行,线程就处于阻塞状态,直到线程转为就绪状态,才有机会重新获取cpu进入运行状态
    5. 死亡:线程以某种方式结束后会进入死亡状态,死亡后的线程无法再进入就绪状态,即无法再次运行
阻塞线程的原因
  • 阻塞的情况分三种:等待阻塞(o.wait -> 等待队列)、同步阻塞(lock - > 锁池)、其他阻塞(sleep/join)
  • 等待阻塞:运行中的线程调用了o.wait方法,JVM会将该线程放入等待队列中,直到有线程调用了notify或者notifyAll方法将其唤醒,才会进入就绪状态
  • 同步阻塞:运行的线程在尝试获取同步锁时,锁已经被其他线程占用,JVM会将该线程放入锁池,当线程获取到锁后可以进入运行状态
  • 其他阻塞:运行的线程执行了sleep方法或join方法或发生了IO请求,JVM会把程序设置为阻塞状态,直到sleep超时或join等待线程终止或超时,IO请求处理完毕,线程会重新转入就绪状态
线程死亡的原因
  • 线程死亡的原因有三种:正常结束、异常结束、调用stop结束
  • 正常结束:run()方法或call()方法执行完毕,程序正常结束
  • 异常结束:线程抛出了一个未捕获的Error或Exception
  • 调用stop:直接调用线程的stop方法结束线程,很可能会导致死锁,不建议使用
终止线程的方式
  • 终止线程的方式有四种:正常结束、使用终止标志结束interrupt()方法结束、stop方法结束(不推荐)

  • 一般来说线程中的run方法执行完就会结束,但是有时候一些线程需要在满足特定条件之后才应该结束,此时可以使用终止标志来控制是否结束线程

    public class MyThread extends Thread { 
      public volatile boolean exit = false;//设置终止标志
      public void run() {
        //执行逻辑
        System.out.println("MyThreadRun");
        //通过终止标志控制线程持续执行,直到满足特定条件,终止标志转变
        while (!exit){
      		//do something
        }
      }
    }
    
  • 使用interrupt()方法终止线程的方式有两种,当线程处于阻塞状态,调用线程的interrupt()方法使其抛出InterruptException异常,捕获到异常后通过break跳出循环结束执行;当线程未处于阻塞状态,则使用线程的isInterrupt()方法判断中断标志来退出循环

    //未阻塞的情况
    class MyThread extends Thread {
        public void run(){
            while (!interrupted()){
                try {
                    System.out.println("run开始");
                    System.out.println(isInterrupted());
                    System.out.println("run休眠5s");
                    Thread.sleep(5000);
                    System.out.println("run休眠结束");
                }catch (InterruptedException e){
                    System.out.println("捕获到异常");
                    e.printStackTrace();
                    break;
                }
            }
        }
    }
    //测试
    @Test
    void testThread() throws InterruptedException{
      MyThread m1 = new MyThread();
      System.out.println("启动m1");
      m1.start();
      System.out.println("主线线程休眠2s");
      Thread.sleep(2000);
      System.out.println("主线程休眠结束,调用 m1.interrupt()");
      m1.interrupt();
    }
    /** 执行结果:
    启动m1
    主线线程休眠2s
    run开始
    false
    run休眠5s
    主线程休眠结束,调用 m1.interrupt()
    捕获到异常
    java.lang.InterruptedException: sleep interrupted
    	at java.base/java.lang.Thread.sleep(Native Method)
    	at cn.itcast.hotel.thread.MyThread.run(ThreadTests.java:59)
    */
    

4.线程的基本方法

常用方法
  • 线程的常用方法有wait()、notify()、notifyAll、sleep()、join()、yield()、interrupt()
  • wait:线程等待,Object中的方法,运行中的线程调用wait()方法后会进入等待状态,直到被其他线程唤醒中断才会返回,此方法会释放同步锁
  • notify:随机唤醒一个wait的线程,Object中的方法,随机唤醒一个当前对象监视器中处于等待状态的线程,唤醒后的线程需要先与其他线程竞争锁
  • notifyAll:唤醒所有wait的线程,Object中的方法,唤醒所有当前对象监视器中处于等待状态的线程,唤醒后的线程需要先竞争锁,谁拿到锁谁先执行
  • sleep:线程休眠,Thread中的方法,运行中的线程调用sleep()方法后会暂停指定的时间,让出cpu给其他线程,但当前线程并不会改变监控状态,也不会释放锁资源,等到暂停的时间结束,又会自动回复到运行状态
  • join:等待其他线程终止,Thread中的方法,当需要其他线程返回结果时使用,在当前线程中调用一个线程的join()方法,则当前线程转为阻塞状态,直到join的线程结束,当前线程转为就绪状态
  • yield:线程让步,Thread中的方法,一般在run()方法中调用,当前线程调用该方法后会让出cpu执行时间片,与其他线程重新竞争cpu
  • interrupt:线程中断,Thread中的方法,运行中的线程在调用这个方法之后并不会直接结束线程,而是改变了线程中的中断标识;处于sleep状态的线程调用该方法后会抛出InterruptedException异常。通过interrupt方法和while循环可以巧妙得控制线程是否终止
其他方法
  • isAlive:判断线程是否存活
  • activeCount:程序中活跃的线程数
  • currentThread:得到当前线程
  • isDaemon:判断线程是否是守护线程
  • setDaemon:设置线程为守护线程
  • setName:为线程设置一个名字

5.线程池

  • Java中的线程池顶级接口为Executor,但实际上Executor只是一个执行线程的工具,只提供了一个execute方法,真正的线程池接口是ExecutorService
Java中的四种线程池(Executors创建线程的四种方式)
  • newSingleThreadExecutor:创建单线程化线程池

    • 单线程化的线程池中只有一个线程,且这个线程的存活时间是无限的

    • 单线程化线程池中的任务是按照提交的顺序依次执行的,当唯一的线程正繁忙时,新提交的任务会进入内部的阻塞队列,这个阻塞队列是无界的

    • 适用于任务按照次序逐个执行的场景

      // 创建单线程-线程池,任务依次执行
      ExecutorService   newScheduledThreadPool = Executors.newSingleThreadExecutor();
      for (int i = 0; i < 5; i++) {
        //创建任务
        Runnable runnable = new Runnable(){
          @Override
          public void run() {
            System.out.println(Thread.currentThread().getName());
          }
        };
        // 将任务交给线程池管理
        newScheduledThreadPool.execute(runnable);
      }
      
  • newFixedThreadPool:创建固定数量的线程池

    • 固定数量线程池在线程数没有达到固定数量时,没提交一个任务线程池会创建一个新线程,直到线程池中的线程数量达到固定数量

    • 线程池中的线程一旦达到固定数量就会保持不变,如果中途有一个线程发生异常而结束,线程池会补充一个新线程

    • 在接收异步任务的执行目标实例时,如果所有线程都处于繁忙状态,新任务会进入无界的阻塞队列

    • 适用于需要任务长期执行或cpu密集型任务场景,但由于任务队列为无界的,如果有超过了线程池最大容量的大量任务需要处理,队列会无限扩大导致服务器资源耗尽

      // 创建定长线程池
      ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(2);
      
      for (int i = 0; i < 5; i++) {
        //创建任务
        Runnable runnable = new Runnable(){
          @Override
          public void run() {
            try {
              Thread.sleep(10);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName());
          }
        };
        // 将任务交给线程池管理
        newFixedThreadPool.execute(runnable);
      }
      
  • newCachedThreadPool:创建可缓存进程池

    • 可根据需要创建新线程,如果以前构造的线程可用会重用这些线程,但是如果线程池中保有的线程数量多于任务数,会回收空闲时间超过60秒的线程

    • 当接收到新的异步任务而所有线程处于繁忙状态,线程池会创建新的线程来处理任务,线程池不会限制限制容量的大小,只要没达到JVM创建线程大小的限制就能一直创建线程

    • 适用于需要快速处理突发性强而耗时短的任务,如Netty的NIO处理场景、Rest API接口的瞬时削峰场景

      // 创建可缓存线程池
      ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
      
      for (int i = 0; i < 5; i++) {
        //创建任务
        Runnable runnable = new Runnable(){
          @Override
          public void run() {
            try {
              Thread.sleep(10);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName());
          }
        };
        newCachedThreadPool.execute(runnable);
      }
      
  • newScheduledThreadPool:创建可调度线程池

    • 可安排任务在给定时间后运行或定期运行

    • 线程数不设上限,可能会导致创建大量的线程而发生OOM

      // 创建支持定时线程池
      ScheduledExecutorService  newScheduledThreadPool = Executors.newScheduledThreadPool(2);
      
      for (int i = 0; i < 5; i++) {
        //创建任务
        Runnable runnable = new Runnable(){
          @Override
          public void run() {
            System.out.println(Thread.currentThread().getName());
          }
        };
        // 将任务交给线程池管理,延迟2秒后才开始执行线程池中的所有任务
        newScheduledThreadPool.schedule(runnable, 2, TimeUnit.SECONDS);
      }
      
  • 总结:

    • newFixedThreadPool和newSingleThreadExecutor: 阻塞队列无界,会堆积大量任务导致OOM(内存耗尽)

    • newCachedThreadPool和newScheduledThreadPool: 线程数量无上界,会导致创建大量的线程,从而导致OOM

    • 建议直接使用线程池ThreadPoolExecutor的构造器

      //标准创建形式
      public class ThreadPoolExecutor extends AbstractExecutorService {
        private volatile int corePoolSize;//核心线程数,即使线程空闲也不会被收回
        private volatile int maximumPoolSize;//线程的上限
        private volatile long keepAliveTime;//线程的最大空闲时长
        private final BlockingQueue<Runnable> workQueue;//任务的排队队列
        private volatile ThreadFactory threadFactory;//新线程的产生方式
        private volatile RejectedExecutionHandler handler;//拒绝策略
      }
      
向线程池提交任务的方式
  • execute:void execute(Runnable command), Executor接口中的方法
    • 只能接收Runnable接口的参数
    • 无返回结果
    • 无法抛出异常
  • submit: Future submit(Callable task)、 Future submit(Runnable task, T result)、Future<?> submit(Runnable task),Executor接口中的方法
    • 可以接收Runnable和Callable接口的参数
    • Callable接口支持返回结果
    • 可以抛出异常

6.拒绝策略

  • 拒绝的情况:线程池已关闭或线程池已满且maximumPoolSize(最大线程数)已满
  • AbortPolicy:拒绝策略,默认策略,新任务会被拒绝并抛出RejectedExecutionException异常
  • DiscardPolicy:抛弃策略,新任务会被直接抛弃且没有异常抛出
  • DiscardOldestPolicy:抛弃老任务策略,将最早进入队列的任务抛弃,从而腾出空间再加入新任务
  • CallerRunsPolicy:调用者执行策略,新任务被添加到线程池时,如果添加失败,提交线程会自己去执行任务,不再使用线程池中的线程

7.ThreadLocal和ThreadLocalMap

ThreadLocal
  • 介绍 :ThreadLocal即线程局部变量,主要作用是同一个线程类的线程实例通过ThreadLocal可以为每个线程实例创建一份自己独有的变量副本,每个Thread实例通过ThreadLocal操作变量时访问的是自己内部的变量副本,在操作这个变量时不会影响到其他线程也不会受到其他线程的影响,从而解决线程安全的问题

  • 同一个ThreadLocal变量所包含的对象在不同的Thread中有不同的副本,且该副本只能当前线程访问

  • ThreadLocal的作用只是操作数据,真正存储数据的是ThreadLocalMap,实际上操作数据时也是调用的ThreadLocalMap的方法

  • set方法源代码详解:

    public void set(T value) {//调用set方法可以传入一个泛型的参数value
      Thread t = Thread.currentThread();//获取当前线程
      ThreadLocalMap map = getMap(t);//获取当前线程中的ThreadLocalMap
      if (map != null) {//如果这个ThreadLocalMap不为空
        map.set(this, value);//存储数据,map中的key为当前ThreadLocal对象,传入的value则封装为一个Entry
      } else {
        createMap(t, value);//如果ThreadLocalMap为空则调用方法初始化map并保存数据
      }
    }
    
  • get方法源代码详解:

    public T get() {//调用get方法可以得到当前ThreadLocal在ThreadLocalMap中对应的值
      Thread t = Thread.currentThread();//获取当前线程
      ThreadLocalMap map = getMap(t);//获取当前线程中的ThreadLocalMap
      if (map != null) {//如果map不为空
        ThreadLocalMap.Entry e = map.getEntry(this);//查找当前ThreadLocal在map中的Entry对象
        if (e != null) {//查找到保存的Entry对象
          @SuppressWarnings("unchecked")
          T result = (T)e.value;//返回Entry的值也即之前set保存的value
          return result;
        }
      }
      return setInitialValue();//map为空时初始化当前ThreadLocal映射的值为null
    }
    
ThreadLocal和Sychronized的区别
  • ThreadLocal和Sychronized有着本质的区别
  • ThreadLocal用于线程之间的数据隔离,每个线程操作的数据是自己线程内部的数据副本,多个线程可以同时操作自己内部的数据副本
  • Sychronized作用在多线程之间数据共享的场景,每个线程在操作数据时会阻塞其他线程,同时只能用一个线程操作这个数据
示例
  • 图示

    image-20221118115831338
  • 代码

public class ThreadTests {
    protected static ThreadLocal<String> name = new ThreadLocal<>();//多个子线程都需要操作name这个变量,但不能互相影响,此时name的值为空
  //打印输出
    public void print(String threadName){
        System.out.println(threadName + ":"
                + ThreadTests.name.get());
    }
    @Test
    void testThread() {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    name.set("lucy");//通过set将name变量的值设置为lucy
                    print("thread-1");
                  	//name.remove();
                    //print("Thread-1移除name");
                }
            }).start();
           new Thread(new Runnable() {
                @Override
                public void run() {
                    //name.set("jack");
                    print("thread-2");
                }
            }).start();
    }
}
/**
第一次输出,此时线程2没有设置name:
thread-1:lucy
thread-2:null
第二次输出,线程1移除name的值,线程2将name设置为lucy
thread-1移除前:lucy
Thread-1移除name:null
thread-2:jack
*/
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值