Java多线程(Thread,Runnable,Callable)附带相关面试题

简介: 1.通过继承Thread类实现多线程,2.多线程常用操作方法,3.通过Runnable接口实现多线程,4.通过Lambda与Thread结合实现快速创建多线程,5.通过实现Callable接口得到线程返回值


1.通过继承Thread类实现多线程

子类通过继承Thread父类并覆写其中的run方法。run方法实现线程需要完成的任务,最后在主类中实例化子类(即创建线程)并调用start()方法,让创建的线程工作。

案例1 售票员在票出售光前实现一直出售:

package Example1401;
class MyThread extends Thread{
    private int ticket = 100;
    @Override
    public void run() {
        while (ticket>0){
            System.out.println("卖出第"+ticket+"张票");
            ticket--;
        }
    }
}
public class javaDemo {
    public static void main(String[] args) {
        MyThread m1 = new MyThread();
        MyThread m2 = new MyThread();
        MyThread m3 = new MyThread();
        m1.start();
        m2.start();
        m3.start();
    }
}

image.gif

image.gif编辑

调用代码后发现代码出现几个问题:

1.售票员售票速度太快了

2.多个售票员之间100张票出现售票混乱的情况,比如只有一张8号的票却同时卖出去了

为了解决这些问题我们就需要引入多线程常用的操作方法

*面试题线程的run()和start()有什么区别?

    1. 每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。
    2. start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。
    3. start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。
    4. run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。

    *面试题为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

    这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!

    new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。

    而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

    总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。


    2.多线程常用操作方法

    方法 描述
    Thread.sleep() 让当前线程休眠指定的时间,暂停执行,不释放锁资源
    Thread.interrupt() 中断当前线程,给线程发送中断信号,该线程需要处理中断信号来决定如何终止执行
    Thread.currentThread().getName() 获取当前线程的名称
    Thread.currentThread().setName(String name) 设置当前线程的名称
    Thread.yield() 暂停当前正在执行的线程,让其他具有相同优先级的线程有机会执行
    Thread.join() 等待指定线程终止执行,当前线程进入阻塞状态,直到指定线程执行结束
    Thread.wait() 在等待其他线程通知之前,使当前线程进入等待状态,并释放对象的监视器锁资源

    首先是解决售票员售票速度太快了问题,查看方法可以发现Thread.sleep()就能解决这个问题

    注意Thread.sleep()会抛出一个异常interruptException表示线程睡眠被打断了。所以需要用try catch去处理这个可能发送的异常

    修改后的案例代码2:

    package Example1401;
    class MyThread extends Thread{
    //    设置只有100张票
        private int ticket = 100;
        @Override
        public void run() {
            while (ticket>0){
                try {
    //                内部数字单位为毫秒 1000毫秒就是1秒
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"卖出第"+(ticket--)+"张票");
            }
        }
    }
    public class javaDemo {
        public static void main(String[] args) {
            MyThread m1 = new MyThread();
            MyThread m2 = new MyThread();
            MyThread m3 = new MyThread();
            m1.setName("售票员1");
            m2.setName("售票员2");
            m3.setName("售票员3");
            m1.start();
            m2.start();
            m3.start();
        }
    }

    image.gif

    image.gif编辑

     接下来就是解决同步问题了,由上图可以看到虽然解决了卖得快得问题,但是并发问题并未解决,并发就是指多个线程同时调用资源,同时修改资源得情况。需要用到同步synchronized实现同步,这里只展示实现代码,具体讲解在下一篇文章 Java多线程(二)_Alphamilk的博客-CSDN博客

    案例代码:(售票员独立售卖电影票并且在随机时间内售出)

    package ExampleThread;
    import java.util.Random;
    class Mythread implements Runnable {
        private int ticket = 100;
        @Override
        public void run() {
            while (true){
    //            当没买完票则开始卖票
                if (ticket > 0) {
    //                实现同步
                    synchronized (this) {
                        try {
    //                        设置随机售卖出去的时间间隔
                            Random random = new Random();
                            int time = random.nextInt(1000);
                            Thread.sleep(time);
    //                        输出售卖信息
                            System.out.println(Thread.currentThread().getName() + "卖第" + ticket-- + "张票");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    if (ticket <= 0){
                        System.out.println("票已经全部卖光了");
                        break;
                    }
                }
    //            线程礼让,让其他线程也有机会售出票
                Thread.yield();
            }
        }
    }
    public class test {
        public static void main(String[] args) {
            Mythread task = new Mythread();
            new Thread(task, "售票员A").start();
            new Thread(task, "售票员B").start();
            new Thread(task, "售票员C").start();
        }
    }

    image.gif

    image.gif编辑

    其他功能 案例:

    1.线程中断

    package Example1408;
    public class javaDemo {
        public static void main(String[] args)throws Exception {
            Thread thread = new Thread(()->{
                System.out.println("睡觉先睡觉100000ms");
                try{
                    Thread.sleep(100000);
                    System.out.println("谁醒了,可以开始工作啦");
                }catch (InterruptedException e){
                    System.out.println("睡觉被打断了,愤怒值+++++");
                }
            });
            thread.start();
            Thread.sleep(1000);
            thread.interrupt();
        }
    }

    image.gif

    image.gif编辑

    2.强制执行

    就是指如果一个线程使用了Thread 对象.join();那么他就是老大,所有资源都独享,其他线程只能等待老大享受完成资源。

    package Example1409;
    public class javaDemo {
        public static void main(String[] args) throws Exception {
            Thread vipThread = new Thread(() -> {
                int ticket = 10;
                while (ticket > 0) {
                    System.out.println("VIP用户得到第" + ticket + "张票");
                    ticket--;
                }
            });
            Thread commonThread = new Thread(() -> {
                try {
                    vipThread.join();
                } catch (InterruptedException e) {
                    System.out.println("普通用户取票发生异常");
                }
                int ticket = 10;
                while (ticket > 0) {
                    System.out.println("普通用户得到第" + ticket + "张票");
                    ticket--;
                }
            });
            vipThread.start();
            commonThread.start();
        }
    }

    image.gif

    image.gif编辑

    3.线程礼让

    Thread.yield();线程会让出一些cpu资源出来,在资源不紧缺时候再调用该程序。但还是可能会出现交替运行的情况

    package Example1410;
    public class javaDemo {
        public static void main(String[] args) {
    //        礼让线程
            Thread thread = new Thread(()->{
                for (int i=0; i<100;i++){
                    Thread.yield();
                    System.out.println("礼让线程进行输出"+i);
                }
            });
            thread.start();
    //        主线程
            for (int i=0;i<5;i++){
                System.out.println("主线程进行输出"+i);
            }
        }
    }

    image.gif

    image.gif编辑

    *面试题Thread 类中的 yield 方法有什么作用?

      1. yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
      2. 结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

      3.通过Runnable接口实现多线程

      Runnable的接口代码:

      @FunctionalInterface
      interface Runnable{
          public void run();
      }

      image.gif

      设置类实现Runnable接口并覆写run()方法,最后在主类先创建类的实例对象,再创建线程对象通过线程的构造函数Thread(Task,"threadName"->名字可选,系统会自动构造名字)实现线程运行Task类。

      案例1:(通过实现Runnable实现售票员售票)

      package Example1403;
      class Mythread implements Runnable{
          private int ticket =100;
          @Override
          public void run() {
              while (ticket>0){
                  System.out.println("出售第"+ticket+"张票");
                  ticket--;
              }
          }
      }
      public class javaDemo {
          public static void main(String[] args) {
              Mythread m1 = new Mythread();
              new Thread(m1).start();
          }
      }

      image.gif

      image.gif编辑 案例代码2:

      package Example1404;
      class Genshin implements Runnable{
          @Override
          public void run() {
              System.out.println("原神,启动!");
          }
      }
      class Wangzhe implements Runnable{
          @Override
          public void run() {
              System.out.println("王者荣耀,启动!");
          }
      }
      class huoyin implements Runnable{
          @Override
          public void run() {
              System.out.println("火影忍者,启动!");
          }
      }
      public class javaDemo {
          public static void main(String[] args) {
              Genshin genshin = new Genshin();
              huoyin huoyin = new huoyin();
              Wangzhe wangzhe = new Wangzhe();
              new Thread(genshin).start();
              new Thread(huoyin).start();
              new Thread(wangzhe).start();
          }
      }

      image.gif

      image.gif编辑

      多线程中Thread与Runnable的区别:

        1. 继承Thread类:这种方式是创建一个新的类,继承自Thread类,并重写它的run()方法来定义线程执行的逻辑。通过创建Thread类的实例对象,可以直接调用其start()方法来启动线程。这种方式的优点是代码简单,方便使用,但缺点是由于Java不支持多重继承,因此如果已经继承了其他类,则无法再使用这种方式创建线程。
        2. 实现Runnable接口:这种方式是创建一个实现Runnable接口的类,在该类中实现run()方法来定义线程执行的逻辑。然后,创建Thread类的实例对象时,将实现了Runnable接口的类的实例对象作为参数传递给Thread的构造函数。最后,调用Thread实例对象的start()方法来启动线程。这种方式的优点是避免了单继承的限制,提高了代码的灵活性和可复用性。

        4.通过Lambda与Thread结合实现快速创建多线程

        当然Thread构造函数的Task可以与Lambda表达式结合实现不需要构造子类就能够实现多线程

        回顾以往的Lambda的知识,核心就两种实现方法

        1.()->{方法体}

        2.()->语句

        案例3

        package Example1405;
        public class javaDemo {
            public static void main(String[] args) {
                new Thread(()->{
                   for (int i =0 ;i<100;i++){
                       System.out.println("感觉不如原神");
                   }
                }).start();
            }
        }

        image.gif

        image.gif编辑

        到现在以上所有Thread,还是Runnable可以发现都没有一个这些类或者接口中的run()方法是没有任何返回值的,为了解决这个问题java提供了Callable接口


        5.通过实现Callable接口得到线程返回值

        由于用Thread会有单继承限制,而用Runnable会有run方法无法获取返回值的缺点,所以为了获取返回值则使用Callable泛型,Callable的接口如下:

        @FunctionalInterface
        interface Callable<V>{
            public V call()throws Exception;
        }

        image.gif

        可以看到Callable使用了泛型类,目的在于可以返回不同类型的值,V可以设置为String,int,double等等类型。并且实现这个类需要用FutureTask进行接收子类

          1. FutureTask <返回类型> Task = new FutureTask<>(实例化创建的任务类的对象)
          2. new Thread(Task,"Threadname").start();
          3. task.get();获取返回值
          package Example1405;
          import java.util.concurrent.Callable;
          import java.util.concurrent.FutureTask;
          class MyThread implements Callable<String>{
              @Override
              public String call() throws Exception {
                  System.out.println("线程,,启动");
                  return "return 返回测试";
              }
          }
          public class javaDemo {
              public static void main(String[] args)throws Exception {
                  FutureTask<String> task = new FutureTask<>(new MyThread());
                  new Thread(task).start();
                  System.out.println(task.get());
              }
          }

          image.gif

          多线程中Runnable与Callable的区别

            1. 返回值类型: Runnable接口的run()方法没有返回值,它的执行结果无法获取。而Callable接口的call()方法可以返回一个结果,并且可以通过Future对象获取该结果。
            2. 异常处理: Runnable接口的run()方法无法抛出受检查异常,只能在方法内部进行异常处理。而Callable接口的call()方法可以抛出受检查异常,需要在方法内部进行异常处理或者通过Future对象获取异常信息。
            3. 支持泛型: Callable接口是一个泛型接口,可以指定call()方法的返回类型。而Runnable接口不支持泛型,无法指定返回类型。
            4. 使用方式: Runnable接口通常与Thread类一起使用,通过创建Thread类的实例并传入一个Runnable对象来创建线程。Callable接口通常与ExecutorService线程池一起使用,通过提交Callable任务给线程池来创建线程,并通过Future对象获取任务的执行结果。

            目录
            相关文章
            |
            21天前
            |
            Java 网络安全 Maven
            Exception in thread "main" java.lang.NoSuchMethodError: okhttp3.OkHttpClient$Builder.sslSocketFactory(Ljavax/net/ssl/SSLSocketFactory;Ljavax/net/ssl/X509TrustManager;)Lokhttp3/OkHttpClient$Builder; 问题处理
            【10月更文挑战第26天】Exception in thread "main" java.lang.NoSuchMethodError: okhttp3.OkHttpClient$Builder.sslSocketFactory(Ljavax/net/ssl/SSLSocketFactory;Ljavax/net/ssl/X509TrustManager;)Lokhttp3/OkHttpClient$Builder; 问题处理
            37 2
            |
            27天前
            |
            Java 开发者
            在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
            【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
            19 3
            |
            27天前
            |
            Java
            在Java多线程编程中,实现Runnable接口通常优于继承Thread类
            【10月更文挑战第20天】在Java多线程编程中,实现Runnable接口通常优于继承Thread类。原因包括:1) Java只支持单继承,实现接口不受此限制;2) Runnable接口便于代码复用和线程池管理;3) 分离任务与线程,提高灵活性。因此,实现Runnable接口是更佳选择。
            35 2
            |
            27天前
            |
            Java
            Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
            【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
            29 2
            |
            27天前
            |
            Java 开发者
            Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
            【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
            33 1
            |
            2月前
            |
            存储 Java 程序员
            优化Java多线程应用:是创建Thread对象直接调用start()方法?还是用个变量调用?
            这篇文章探讨了Java中两种创建和启动线程的方法,并分析了它们的区别。作者建议直接调用 `Thread` 对象的 `start()` 方法,而非保持强引用,以避免内存泄漏、简化线程生命周期管理,并减少不必要的线程控制。文章详细解释了这种方法在使用 `ThreadLocal` 时的优势,并提供了代码示例。作者洛小豆,文章来源于稀土掘金。
            |
            1月前
            |
            存储 消息中间件 资源调度
            C++ 多线程之初识多线程
            这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
            44 1
            C++ 多线程之初识多线程
            |
            27天前
            |
            Java 开发者
            在Java多线程编程中,选择合适的线程创建方法至关重要
            【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
            17 2
            |
            27天前
            |
            安全 Java 开发者
            Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
            本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
            34 1
            |
            27天前
            |
            Java
            在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
            在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
            25 1
            下一篇
            无影云桌面