多线程详解


1、基本概念:程序、进程、线程

程序

  • 完成特定任务、用某种语言编写的一组指令的集合。一段静态的代码,静态对象。

进程

  • 程序的一次执行过程,或是正在运行的一个程序,动态的过程:产生、存在、和消亡
  • 程序是静态的,进程是动态的
  • 进程是资源分配的单位

线程

  • 线程是程序内部的一条执行路径

  • 一个进程,同一时间并行多个线程,多线程

  • 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器

  • 存在安全问题

  • 一个java应用程序,至少有三个线程:main()主线程,

使用多线程的优点

  1. 提高应用程序的响应,增强用户体验
  2. 提高计算机系统CPU的利用率
  3. 改善程序结构,将复杂的进程分为多个线程,独立运行

何时需要多线程

  • 程序需要同时执行两个或多个任务
  • 程序实现一些需要等待的任务时,如:用户输入,文件读写操作、网络操作
  • 需要后台运行的程序

单核CPU和多核CPU

单核CPU

  • 假的多线程,在一个时间单元内,只能执行一个线程的任务
  • 实际上是单线程,运行中将其他进程挂起,执行,执行速度很快,所以在使用上感觉是多线程。

多核CPU

  • 真正意义上的多线程

并行与并发

  • 并行:多个CPU同时执行多个任务
  • 并发:一个CPU “同时” 执行多个任务,如:秒杀

2、线程的创建和使用

线程的创建

  • 方式一

    • 继承于Thread类

      步骤:

      1. 创建一个继承于Thread的子类
      2. 重写Thread类的run()方法–》此线程执行的操作
      3. 创建Thread类的子类对象(可使用匿名对象)
      4. 通过此对象调用start()
      5. 若要再创建新的线程,需创建新的实例来调用start()方法
      //例:遍历100以内的所有偶数
          //1.创建一个继承于Thread类的子类
      public class MyThread extends Thread{
          //2.重写Thread类的run()
          @Override
          public void run() {
              for (int i = 0; i <=1000 ; i++) {
                  if(i%2==0){
                      System.out.println(Thread.currentThread().getName()+":"+i);
                  }
              }
          }
      }
      class ThreadTest {
          public static void main(String[] args) {
              //3.创建Thread类的子类的对象
             MyThread myThread= new MyThread();
             //4.通过此对象调用start()①启动当前线程②调用当前线程的run()
             myThread.start();
              //  如下操作任然是在main()线程执行的
              for (int i = 0; i <=1000 ; i++) {
                  if(i%2!=0){
      //                Thread.currentThread().getName()获取线程名
                      System.out.println(Thread.currentThread().getName()+":"+i);
                  }
              }
          }
      }
      
  • 方式二

    • 实现Runnable接口

      步骤:

      1. 创建一个实现了Runnable接口的类

      2. 实现类中实现Runnable中的抽象方法:run()

      3. 创建实现类的对象

      4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象

      5. 通过Thread类的对象调用start()

        //1. 创建一个实现了Runnable接口的类
         class MIThread implements Runnable{
        //     2. 实现类中实现Runnable中的抽象方法:run()
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if(i%2==0){
                        System.out.println(Thread.currentThread().getName()+":"+i);
                    }
                }
            }
        }
        class ThreadTes{
            public static void main(String[] args) {
        //        3. 创建实现类的对象
              MIThread mi=  new MIThread() ;
        //        4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
              Thread t1=new Thread(mi);
        //        5. 通过Thread类的对象调用start(),启动当前线程,调用当前线程的run方法,底层是Thread含有Runnable类型参数的构造器
              t1.start();
        //      再启动一个线程
                Thread t2=new Thread(mi);
                t2.start();
            }
        }
        
  • 比较两种创建方式

    • 开发中优先选择,实现Runnable接口的方式

    • 原因:

      1. 实现的方式没有类的单继承的局限
      2. 实现方式更适合处理多线程有共享数据的情况
    • 联系: public class Thread implements Runnable

      相同点:两种方式都需要重写run() 将线程 要执行的逻辑声明在run()中;

线程的调度

  • 调度策略
    • 时间片
    • 抢占式,高优先级的线程抢占CPU
  • java的调度方法
    • 先来先服务,先进先出队列
    • 高优先级,优先调度的抢占式策略

线程优先级

  • 优先级

    • MAX_PRIORITY : 10
    • MIN_PRIORITY: 1
  • NORM_PRIORITY : 5

  • 获取和设置当前线程的优先级

    • getPriority()
    • setPriority( int p)
  • 高优先级的线程要抢占低优先级线程的CPU执行权,只是从概率上讲,高优先级有很高概率会执行,但并不意味着高优先级的线程执行完,低优先级的才执行;

3、线程的生命周期

线程生命周期的五种状态

  • 新建:Thread类或子类对象被声明创建时

  • 就绪:新建的线程被start()后,获取了其他所有的资源,只是没分配到CPU资源

  • 运行:获取CPU资源,执行

  • 阻塞:某种特殊情况下,让出CPU并临时终止自己的执行

  • 死亡:完成它的全部工作或线程提前被强制性的终止,异常导致结束;

  • 状态图

4、线程同步

  • 多个线程操作共享数据时,出现的线程安全问题

  • 解决方式:

    • 方式一:同步代码块—不能包含多了,也不能包含少了

      synchronized(同步监视器){
          //需要被同步的代码
      }
      
      • 说明:操作共享数据的代码==需要被同步的代码
        共享数据:多个线程共同操作的变量

      • 同步监视器:锁,任何一个类的对象都可充当为锁

        要求:多个线程必须要共用同一把锁

        • 对于继承Thread类创建多线程方法

          1. 同步监视器可以考虑使用定义静态类对象
          2. 可以考虑使用当前类对象synchronized (类名.class)
        • 对于实现Runnable接口创建多线程方法

          1. 同步监视器可以考虑使用 this
    • 方式二:同步方法: 如果操作共享数据的代码完整的声明在一个方法中,将此方法声明为同步的

      private synchronized void show(){ 
          //同步监视器 this
          
      }
      private static synchronized void show(){ 
          //同步监视器:当前类本身    
      }
      
      1. 同步方法仍然涉及到同步监视器,只是不需要显式的声明

      2. 非静态的同步方法,同步监视器是:this

        静态的同步方法,同步监视器是:当前类本身

    • 方式三:Lock(锁) jdk5.0新增

      1. 实例化ReentrantLock

      2. 调用lock()方法

      3. 调用解锁方法unlock()

        package com.han.java.ere;
        
        import java.util.concurrent.locks.ReentrantLock;
        
        /**
         * 解决线程安全的方式三:lock--- jdk5.0新增
         * @author hlrstart
         * @create 2021-04-04-9:35
         */
        public class LockTest {
            public static void main(String[] args) {
                Windw win=new Windw();
                Thread t1=new Thread(win);
                Thread t2=new Thread(win);
                Thread t3=new Thread(win);
                t1.setName("窗口一");
                t2.setName("窗口二");
                t3.setName("窗口三");
                t1.start();
                t2.start();
                t3.start();
            }
        }
        class Windw implements  Runnable{
            private int sticket=100;
        //    1.实例化ReentrantLock
             private ReentrantLock lock=new ReentrantLock() ;
            @Override
            public void run() {
                while(true){
                    try {
        //              2.调用lock()方法
                        lock.lock();
                        if (sticket > 0) {
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            System.out.println(Thread.currentThread().getName() + ": 票号为" + sticket);
                            sticket--;
                        } else {
                            break;
                        }
                    }finally {
        //          3.调用解锁方法
                        lock.unlock();
                    }
                }
            }
        }
        
  • 同步的方式优缺点

    • 优点:解决了线程安全问题
    • 局限性:操作同步代码时,是单线程的过程,效率低
  • 线程死锁问题

    • 不同的线程分别占用对方需要的资源不放弃,都在等待对方释放自己需要的资源。

    • 出现死锁,不会出现异常,不会出现提示,所有线程都处于阻塞状态,无法继续。

5、线程通信

涉及到的三个方法

  • wait():一旦执行此方法,当前线程进入阻塞状态,并释放同步监视器

  • notify():一旦执行此方法,就会唤醒被wait()的一个线程,若有多个线程被wait(),就唤醒优先级高的那一个

  • notifyAll() :一旦执行此方法,就会唤醒所有被wait()的线程

    说明

    1. wait()、notify()、notifyAll() 三个方法必须使用在同步代码块或同步方法中。
    2. wait()、notify()、notifyAll() 三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则会出现异常。
    3. wait()、notify()、notifyAll() 三个方法是定义在java.lang.Object类中。

6、JDK5.0新增线程创建方式

新增方式一:实现Callable接口

  • 与使用Runnable相比,Callable功能更强大些

    • call()方法可以有返回值
    • call()方法可以抛出异常
    • 支持泛型的返回值
    • 需要借助FutureTask类,获取返回结果
  • 实现步骤

    1. 创建一个实现Callable的实现类
    2. 实现call方法,将此线程需要执行的操作声明在call()中
    3. 创建Callable接口实现类的对象
    4. 将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask 的对象
    5. 将FutureTask 的对象作为参数传递到Thread类的构造器中,创建Thread对象,调用start()
    6. 获取Callable中call方法的返回值
    package com.han.createThread;
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    
    /**
     * 创建线程的方式三:实现Callable接口  ---jdk5.0 新增
     *
     * @author hlrstart
     * @create 2021-04-04-11:24
     */
    
    //1.创建一个实现Callable的实现类
    class NumThread implements Callable{
    //   2. 实现call方法,将此线程需要执行的操作声明在call()中
        @Override
        public Object call() throws Exception {
            int sum=0;
            for (int i = 1; i <=100 ; i++) {
                if(i%2==0){
                    System.out.println(i);
                    sum+=i;
                }
            }
            return sum;
        }
    }
    public class ThreadNEw {
        public static void main(String[] args) {
           // 3.创建Callable接口实现类的对象
            NumThread num=new NumThread();
      //4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask 的对象     
            FutureTask f=new FutureTask(num);
      //5.将FutureTask 的对象作为参数传递到Thread类的构造器中,创建Thread对象,调用start()
            new Thread(f).start();
            try {
    //   6.         get()方法的返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
              Object sum=  f.get();
                System.out.println("总和为:"+sum);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    
    }
    

新增方式二:使用线程池(开发中使用)

  • 思想:提前创建好多个线程,放入线程池,使用时直接获取,使用完放回池中。

  • 好处:

    • 提高响应速度(减少创建新线程的时间)
    • 降低资源消耗(重复利用)
    • 便于线程管理
  • 实现步骤

    1. 提供指定线程数量的线程

    2. 执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象

      class Num implements Runnable{
          @Override
          public void run() {
              for (int i = 0; i <=100; i++) {
                  if(i%2==0){
                      System.out.println(Thread.currentThread().getName()+":"+i);
                  }
              }
          }
      }
      public class ThreadPool {
          public static void main(String[] args) {
              //1.提供指定线程数量的线程
              ExecutorService service = Executors.newFixedThreadPool(10);
              //2.执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
              service.execute(new Num()); //适用于Runnable
      //        service.submit(Callable callable); //适用于callable
          }
      }
      

7、涉及到的面试题

  1. sleep()和wait()的异同?、

    相同点:

    • 执行该方法,都可以使当前线程进入阻塞状态

    不同点:

    1. 两个方法的定义位置不同:Thread类中定义sleep(),Object类中定义wait()
    2. 调用的要求不同:sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块或同步方法中
    3. 是否会释放同步监视器:若两个方法都使用在同步代码块或同步方法中,sleep()不会释放(例如睡眠中,厕所门依然没有打开),wait()会释放
  2. 面试题:synchronized 与Lock的异同

    相同:

    • 都可以解决线程安全问题

    不同点:

    • synchronized机制在执行完同步代码之后,自动释放同步监视器

    • Lock需要手动启动同步(lock()),同时结束同步也需要手动的实现(unlock())


文章作者: 时光路人
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 时光路人 !
评论
  目录