Java面试题(6)- 多线程

Java面试题(6)- 多线程

1 说说有几种方法创建线程,各自的优缺点是什么?

  • 继承Thread类:Java语言是单继承,如果继承了Thread就不能继承其他类。

    //1.创建一个继承于Thread类的子类
    class MyThread extends Thread{
    //2.重写Thread类的run()
    @Override
    public void run() {
       for(int i = 0;i < 10;i++){
           if(i % 2 == 0){
               System.out.println(i);
           }
       }
    }
    }
  • 实现Runnable接口:通常不直接在类上实现runnable接口,与类的耦合度高。

    class MThread 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);
            }
        }
    }
    }
    public class ThreadTest {
    public static void main(String[] args) {
        //3.创建实现类的对象
        MThread mThread = new MThread();
        //4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(mThread);
        t1.setName("线程1");
        //5.通过Thread类的对象调用start():A.启动线程B.调用当前线程的run()-->调用了Runnable类型的target
        t1.start();
        //再启动一个线程,遍历100以内的偶数//只需重新实现步骤4,5即可
        Thread t2 = new Thread(mThread);
        t2.setName("线程2");
        t2.start();
    }
    }
  • 实现Callable接口:与futrue配合使用,相比实现runnable接口,callable()有返回值,且自带异常处理。

    //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;//sum是int,自动装箱为Integer(Object的子类)
    }
    }
    public class ThreadTest {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        //4.将此Callable接口实现类的对象作为参数传递到 FutureTask的构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);
        //5.将 FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        new Thread(futureTask).start();
        try {
            //获取Callable中call()的返回值(不是必须的步骤)
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
            Object sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
    }
  • 使用线程池:降低资源消耗,重复利用线程池中线程,不需要每次都创建。

    class NumberThread implements Runnable{
    
    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
    }
    class NumberThread1 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);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        //2.执行指定的线程操作。需要提供实现Runnable 接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适用于Runnable
        service.execute(new NumberThread1());//适用于Runnable
        //3.关闭连接池
        service.shutdown();
    }
    }

2 启动一个线程是用run还是start?

启动线程肯定要用start()方法。当用start()开始一个线程后,线程就进入就绪状态,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。这并不意味着线程就会立即运行。当cpu分配给它时间时,才开始执行run()方法(如果有的话)。START()是方法,它调用RUN()方法,而RUN()方法是必须重写的,run()方法中包含的是线程的主体。

3 说说线程和进程的差别是什么?

  • 定义不一样,进程是执行中的一段程序,而一个进程中执行中的每个任务即为一个线程。
  • 一个线程只可以属于一个进程,但一个进程能包含多个线程。
  • 线程无地址空间,它包括在进程的地址空间里。
  • 线程的开销或代价比进程的小。

4 说说守护线程有什么用?

守护线程是一种支持性线程,主要用于后台调度以及支持性的工作。守护线程具备自动结束生命周期的特性,而非守护线程则不具备。 GC垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

public class MyDaemon implements Runnable {
    @Override
    public void run() {
        System.out.println("********守护线程********");
    }
}
public class MyDaemonTest {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyDaemon(), "守护线程");
        thread.setDaemon(true);
        thread.start();
        System.out.println("******main线程执行结束******");
    }
}

5 如何实现两个线程串行执行?

  • 在子线程中通过join()方法指定顺序

    public class ThreadJoinDemo {
    public static void main(String[] args) throws InterruptedException {
        final Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("打开冰箱!");
            }
        });
    
        final Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    thread1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("拿出一瓶牛奶!");
            }
        });
    
        final Thread thread3 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    thread2.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("关上冰箱!");
            }
        });
    
        //下面三行代码顺序可随意调整,程序运行结果不受影响,因为我们在子线程中通过“join()方法”已经指定了运行顺序。
        thread3.start();
        thread2.start();
        thread1.start();
    }
    }
  • 主线程中通过join()方法指定顺序

    public class ThreadMainJoinDemo {
    public static void main(String[] args) throws InterruptedException {
        final Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("打开冰箱!");
            }
        });
    
        final Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("拿出一瓶牛奶!");
            }
        });
    
        final Thread thread3 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("关上冰箱!");
            }
        });
    
        thread1.start();
        thread1.join();
        thread2.start();
        thread2.join();
        thread3.start();
    }
    }
  • 通过倒数计时器CountDownLatch

    public class ThreadCountDownLatchDemo {
    
    private static CountDownLatch countDownLatch1 = new CountDownLatch(1);
    
    private static CountDownLatch countDownLatch2 = new CountDownLatch(1);
    
    public static void main(String[] args) {
        final Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("打开冰箱!");
                countDownLatch1.countDown();
            }
        });
    
        final Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    countDownLatch1.await();
                    System.out.println("拿出一瓶牛奶!");
                    countDownLatch2.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    
        final Thread thread3 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    countDownLatch2.await();
                    System.out.println("关上冰箱!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    
        //下面三行代码顺序可随意调整,程序运行结果不受影响
        thread3.start();
        thread1.start();
        thread2.start();
    }
    }
  • 创建单一化线程池newSingleThreadExecutor()

    public class ThreadPoolDemo {
    
    static ExecutorService executorService = Executors.newSingleThreadExecutor();
    
    public static void main(String[] args) {
        final Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("打开冰箱!");
            }
        });
    
        final Thread thread2 =new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("拿出一瓶牛奶!");
            }
        });
    
        final Thread thread3 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("关上冰箱!");
            }
        });
        executorService.submit(thread1);
        executorService.submit(thread2);
        executorService.submit(thread3);
        executorService.shutdown();
    }
    }

6 一个线程调用Start()两次会怎么样?

Java 的线程是不允许启动两次的,第二次调用必然会抛出 IllegalThreadStateException,这是一种运行时异常,多次调用 start 被认为是编程错误。

7 谈谈线程的生命周期和状态转移?

线程状态被明确定义在其公共内部枚举类型java.lang.Thread.State中,分别是:

  • 新建(NEW),表示线程被创建出来还没真正启动的状态,可以认为它是个Java内部状态。
  • 就绪( RUNNABLE),表示该线程已经在wM中执行,当然由于执行需要计算资源,它可能是正在运行,也可能还在等待系统分配给它CP∪片段,在就绪队列里面排队。
  • 运行(Running)在其他一些分析中,会额外区分一种状态 RUNNING,但是从 Java aPi的角度,并不能表示出来。
  • 阻塞( BLOCKED),这个状态和我们前面两讲介绍的同步非常相关,阻塞表示线程在等待 Monitor lock。比如,线程试图通过synchronized去获取某个锁,但是其他线程已经独占了,那么当前线程就会处于阻塞状态。
  • 等待( WAITING),表示正在等待其他线程釆取某些操作。一个常见的场景是类似生产者消费者模式,发现任务条件尚未满足,就让当前消费者线程等待(wait),另外的生产者线程去准备任务数据,然后通过类似 notify等动作,通知消费线程可以继续工作了。Thread join(也会令线程进入等待状态。
  • 计时等待( TIMED_WAIT),其进入条件和等待状态类似,但是调用的是存在超时条件的方法,比如wait或join等方法的指定超时版本。
  • 终止( TERMINATED),不管是意外退出还是正常执行结束,线程已经完成使命,终止运行,也有人把这个状态叫作死亡在第二次调用 start()方法的时候,线程可能处于终止或者其他(非NEW)状态,但是不论如何,都是不可以再次启动的。
    线程生命周期

8 线程的sleep和wait方法有什么区别?

  • sleep是线程类(Thread)的方法;wait是Object类的方法
  • sleep是使线程休眠,不会释放对象锁;wait是使线程等待,释放锁
  • sleep让出的是CPU,如果此时代码是加锁的,那么即使让出了CPU,其他线程也无法运行,因为没有得到锁;wait是让自己暂时等待,放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。
  • 调用sleep进入阻塞状态;调用wait进入等待状态,调用notify进入就绪状态。

9 线程的notify和notifyAll有什么区别

  • 如果线程调用了对象的wait方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁;
  • 当有线程调用了notifyAll方法(唤醒所有wait线程)或notify方法(只随机唤醒一个wait线程),被唤醒的线程便会进入该对象的锁池中,所持中的线程回去竞争该对象锁,也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争;
  • 所谓唤醒线程,就是将线程由等待池移动锁池,notifyAll调用后,会将全部线程由等待池移动到锁池,然后参与锁池竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。notify只会唤醒一个线程。

10 上下文切换是什么含义?

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

上下文切换其实际含义是任务切换, 或者CPU寄存器切换。当多任务内核决定运行另外的任务时, 它保存正在运行任务的当前状态, 也就是CPU寄存器中的全部内容。这些内容被保存在任务自己的堆栈中, 入栈工作完成后就把下一个将要运行的任务的当前状况从该任务的栈中重新装入CPU寄存器, 并开始下一个任务的运行, 这一过程就是context switch。

上下文切换通常是计算密集型的。它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

11 如何中断一个正在运行中的线程?

Java里面实现的线程,最终的执行和调度都是由操作系统来决定的,JVM只是对操作系统层面的线程做了一层包装而已。通过start方法启动一个线程的时候,只是告诉操作系统这个线程可以被执行。

在Linux里面使用kill命令,或者采用Java Thread提供的stop方法都可以终止线程,但是这两种方式都是不安全的,因为有可能线程的任务还没有完成,导致运行结果不正确。要想安全的中断一个正在运行的线程,只能在线程内部埋下一个钩子,外部程序通过这个钩子来触发线程的中断命令。在Java Thread里面提供了一个interrupt()方法,这个方法配合isInterrupted()方法使用,就可以实现安全的中断。这种实现方法并不是强制中断,而是告诉正在运行的线程,你可以停止了。不过是否要中断,取决于线程本身和系统的调度,因为线程必须保证运行结果的安全性。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注