美文网首页Java面试都问啥?
02.关于线程你必须知道的8个问题(上)

02.关于线程你必须知道的8个问题(上)

作者: 王有志 | 来源:发表于2022-10-25 21:28 被阅读0次

    大家好,我是王有志,欢迎来到《Java面试都问啥?》的第一篇技术文章。

    这个系列会从Java部分开始,接着是MySQL和Redis的内容,同时会继续更新数据结构与算法的部分,这样在第一阶段,我们就完成了面试“三幻神”的挑战。

    Java的部分从并发编程开始,接着是Java虚拟机,最后是集合框架。至于Java基础,因为大部分只是API的使用,所以只提供整理好的题目,而涉及到反射,动态代理等内容,会在集合框架完成后补充。

    那么话不多说,我们直接开始吧。

    并发编程都问啥?

    每个模块开始时,我都会放出这一模块中知识点的统计数据,供大家参考。

    统计中,我将并发编程分为了5个知识点:

    • 线程基础:线程的基本概念,Thread类的使用等;
    • 线程池:线程池的原理,线程池的使用等;
    • synchronized:原理,锁升级,优化等;
    • volatile:原理,指令重排,JMM相关等;
    • ThreadLocal:原理,使用方法,内存泄漏等;
    • JUC:Lock接口,并发容器,CAS,AQS等。

    统计到并发编程关键词174次,线程出现37次,线程池出现22次,synchronized出现30次,volatile出现12次,ThreadLocal出现8次,JUC出现44次,剩余21次仅提到多线程/并发编程。


    并发编程统计数据.png

    从图中看,ThreadLocal和volatile出现概率较低,但个人建议面试准备中,并发编程的部分要全量准备

    数据大家都看到了,接下来看看各大公司都会问哪些关于线程的问题。这部分题目主要收集自某准网面经,浅紫色底色的题目是我和小伙伴在面试过程遇到过的。

    MarkDown的表格实在太丑了,偷个懒使用图片代替了,文末附上整理后Excel的获取方式。


    线程面试题目.png

    关于线程你必须知道的8个问题

    涉及到概念性的题目就不过多赘述了,这些可以通过百度百科获取到答案。在这里我挑选了8道比较有代表性的问题,和大家分享我的理解。

    并发编程的3要素

    并发编程的3要素:

    • 原子性:操作不可分割,要么不间断的全部执行,要么全部不执行;
    • 有序性:指程序按照代码的顺序结构执行;
    • 可见性:当一个线程修改了共享变量后,其它线程也是立即可见的。

    概念很简单,我们写一些代码展示下有序性和可见性的问题(原子性实在没有想到很好的例子,有没有小伙伴提供示例呢)。

    有序性问题

    public static class Singleton {
        private Singleton instance;
        public Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
      
        private Singleton() {
        }
    }
    

    这是有序性问题的经典案例--未做同步控制的单例模式。当instance还未初始化时,多个线程同时调用getInstance方法,很容易出现其中一个线程获取到的instance为NULL。

    这里涉及Java创建对象的操作,CPU时间片分配的问题,解决它的办法也有很多,暂时按下不表,放到volatile关键字的内容中详细解释。

    可见性问题

    private static boolean flag = true;
    
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (flag) {
            }
            System.out.println("线程:" + Thread.currentThread().getName() + ",flag:" + flag);
        }, "block_thread").start();
    
        TimeUnit.MICROSECONDS.sleep(500);
        
        new Thread(() -> {
            flag = false;
            System.out.println("线程:" + Thread.currentThread().getName() + ",flag:" + flag);
        }, "change_thread").start();
    }
    

    很明显,在change_thread中修改了flag,并不会使block_thread得到解脱,这就是共享变量在线程间不可见的问题。

    Java创建线程的方式

    通常网上的资料会给出4种创建线程的方式:

    • 继承Thread类
    • 实现Runnable接口
    • 实现Callable接口
    • 通过线程池创建

    先不评价这个答案的正确性,我们先来看看继承Thread类,实现Runnable接口和实现Callable接口是如何使用的。

    继承Thread类

    public class ByThread {
        public static void main(String[] args) throws InterruptedException {
            System.out.println("main的线程:" + Thread.currentThread().getName());
            MyThread myThread = new MyThread();
            myThread.start();
        }
        
        static class MyThread extends Thread {
            @Override
            public void run() {
                System.out.println("MyThread的线程:" + Thread.currentThread().getName());
            }
        }
    }
    

    继承Thread类要实现run方法,用于完成业务逻辑,该方法来自于Runnable接口。启动线程通过Thread.start方法,方法内通过调用native方法start0来启动线程。

    实现Runnable接口

    public class ByRunnable {
        public static void main(String[] args) {
            System.out.println("main的线程:" + Thread.currentThread().getName());
            new Thread(new MyRunnable()).start();
        }
    
        static class MyRunnable implements Runnable {
            @Override
            public void run() {
                System.out.println("MyRunnable的线程:" + Thread.currentThread().getName());  
            }
        }
    }
    

    实现Runnable接口同样要实现run方法,启动线程依旧是通过Thread.start方法。

    实质上继承Thread类和实现Runnable接口没有差别,只不过是隔代实现run方法还是直接实现run方法。

    实现Callable接口

    public class ByCallable  {  
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            System.out.println("main的线程:" + Thread.currentThread().getName());
            Callable<String> callable = new MyCallable();
            FutureTask <String> futureTask = new FutureTask<>(callable);
            new Thread(futureTask).start();
            System.out.println("MyCallable的执行线程:" + futureTask.get());
        }
        
        static class MyCallable implements Callable<String> {
            @Override
            public String call() {
                System.out.println("MyCallable的线程:" + Thread.currentThread().getName());
                return Thread.currentThread().getName();
            }
        }
    }
    

    实现Callable接口看起来会复杂一些,但通过代码可以看出来,最终还是回归到Thread.start方法,根据经验,这种方式是不是和Runnable有关系?

    另外,我们注意到这种方式中借助到了FutureTask类,来看看FutureTask的继承关系:

    FutureTask继承关系.png

    不出所料,FutureTask同样要实现Runnable.run方法,只不过这次由FutureTask实现,FutureTaskrun方法中调用Callable.call方法来执行业务逻辑。

    我们来回顾下这3种方式的特点,启动线程都是通过Thread.start方法,start方法的基本执行单位是Runnable接口,它们直接的差异在于如何实现Runnable.run方法。另一个差异就是Callable.call方法是有返回值的,而Runnable.run方法没有返回值。

    使用线程池

    public class ByThreadPool {
        public static void main(String[] args) {
            System.out.println("main的线程:" + Thread.currentThread().getName());
            ExecutorService executorService = Executors.newSingleThreadExecutor();
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程池的线程:" + Thread.currentThread().getName());
                }
            };
            executorService.execute(runnable);
            executorService.shutdown();
        }
    }
    

    使用线程池依旧离不开Runnable.run方法,会不会和Callable一样本质上还是Thread.start

    如果不熟悉ThreadPoolExecutor源码的话,可以采用断点的方式去跟踪源码,重点关注ThreadPoolExecutor.executeThreadPoolExecutor.addWorker两个方法。

    我们可以在addWorker方法中发现两行关键代码:

    final Thread t = w.thread;
    t.start();
    

    这证实了关于ThreadPoolExecutor底层调用的猜想,最终依旧是通过Thread.start方法启动。

    回到最初的问题,Java有几种创建线程的方式?

    如果从Java的层面来看,可以认为创建Thread类的实例对象就完成了线程的创建,而调用Thread.start0可以认为是操作系统层面的线程创建和启动。

    至于网上说的4种创建线程的方式,个人认为将它们归类到线程中业务逻辑的实现方式更合理。

    Java的线程状态

    Java中定义了6种线程状态(与OS的线程状态有差别),线程状态的枚举类被定义为Thread的内部类State

    public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }
    

    需要注意,Java中并未定义线程的RUNNING状态,而是通过RUNNABLE包含了RUNNABLE(可运行)和RUNNING(运行中)

    建议大家阅读源码中的注释,很清晰的解释了每个状态的场景。下面我还是通过几段代码展示线程的不同状态。

    Tips:代码中出现的TimeUnit.MILLISECONDS.sleep是为了确保线程已经进入期望的状态,如果不能很好的理解,文末附有Gitee地址,工程中的代码有注释。

    常规状态的转换

    这里指的是线程从创建后(NEW),到启动后(RUNNABLE),再到最后终止(TERMINATED)的一种无竞争的线程状态转换。

    • NEW(新建):创建线程后尚未启动(未调用start方法);
    • RUNNABLE(可运行):可运行状态的线程在Java虚拟机中等待调度线程选中获取CPU时间片;
    • TERMINATED(终止):线程执行结束。

    写一段简单的代码来看下:

    public class NormalStateTransition {
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            System.out.println("线程[" + thread.getName() + "]创建,状态:[" + thread.getState() + "]");
            thread.start();
            System.out.println("线程[" + thread.getName() + "]启动,状态:[" + thread.getState() + "]");
            TimeUnit.SECONDS.sleep(2);
            System.out.println("线程[" + thread.getName() + "]结束,状态:[" + thread.getState() + "]");
        }
    }
    

    代码非常简单,这里就不再解释了。

    常规状态的转换:


    常规状态的转换.png

    阻塞状态的转换

    阻塞状态是一种“异常”的状态,通常是在等待资源。

    BLOCKED(阻塞):等待监视器锁而阻塞的线程状态,处于阻塞状态的线程正在等待监视器锁进入同步的代码块/方法,或者在调用Object.wait之后重新进入同步的代码块/方法。

    再写一段代码:

    public class BlockedStateTransition {
        public static void main(String[] args) throws InterruptedException {
            AtomicBoolean locker = new AtomicBoolean(false);
            new Thread(() -> {
                synchronized (locker) {
                    try {
                        TimeUnit.MILLISECONDS.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
      
            Thread t = new Thread(() -> {
                synchronized (locker) {
                    try {
                        TimeUnit.MILLISECONDS.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程[" + Thread.currentThread().getName() + "]阻塞后,状态:[" + Thread.currentThread().getState() + "]");
                }
            });
            System.out.println("线程[" + t.getName() + "]创建,状态:[" + t.getState() + "]");
            t.start();
            System.out.println("线程[" + t.getName() + "]启动,状态:[" + t.getState() + "]");
            System.out.println("线程[" + t.getName() + "]阻塞中,状态:[" + t.getState() + "]");
            TimeUnit.MILLISECONDS.sleep(5000);
            System.out.println("线程[" + t.getName() + "]结束,状态:[" + t.getState() + "]");
        }
    }
    

    首先是匿名线程持有locker,接着线程t启动,进入RUNNABLE状态,线程t尝试获取locker,进入BLOCKED状态,等待后获取到locker,进入RUNNABLE状态,最后执行结束,进入TERMINATED状态。

    阻塞状态转换:


    阻塞状态的转换.png

    等待状态的转换

    关于等待状态,Java源码的注释有详细描述如何进入等待状态,以及如何唤醒处于等待状态的线程:

    Thread state for a waiting thread. A thread is in the waiting state due to calling one of the following methods:
    Object.wait with no timeout
    Thread.join with no timeout
    LockSupport.park
    A thread in the waiting state is waiting for another thread to perform a particular action. For example, a thread that has called Object.wait() on an object is waiting for another thread to call Object.notify() or Object.notifyAll() on that object. A thread that has called Thread.join() is waiting for a specified thread to terminate.

    WAITING(等待):线程处于等待状态,处于等待状态的线程正在等待另一个线程执行的特定操作(通知或中断)。

    再再写一段代码:

    public class WaitingStateTransition {
        public static void main(String[] args) throws InterruptedException {
            AtomicBoolean locker = new AtomicBoolean(false);
            Thread t = new Thread(() -> {
                synchronized (locker) {
                    try {
                        TimeUnit.MILLISECONDS.sleep(100);
                        locker.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程[" + Thread.currentThread().getName() + "]唤醒,状态:[" + Thread.currentThread().getState() + "]");
                }
            });
            System.out.println("线程[" + t.getName() + "]创建,状态:[" + t.getState() + "]");
            t.start();
            System.out.println("线程[" + t.getName() + "]启动,状态:[" + t.getState() + "]");
            TimeUnit.MILLISECONDS.sleep(150);
            System.out.println("线程[" + t.getName() + "]等待,状态:[" + t.getState() + "]");
      
            new Thread(() -> {
                synchronized (locker) {
                    locker.notify();
                }
            }).start();
            TimeUnit.MILLISECONDS.sleep(100);
            System.out.println("线程[" + t.getName() + "]结束,状态:[" + t.getState() + "]");
        }
    }
    

    线程t创建后,进入NEW状态,启动后,进入RUNNABLE状态,locker.wait后,进入WAITING状态,匿名线程启动,locker.notify后,唤醒线程t,进入RUNNABLE状态,最后线程执行结束,进入TERMINATED状态。

    等待状态的转换:


    等待状态的转换.png

    限时等待状态的转换

    Java源码的注释上,也很详细的解释了如何进入限时等待:

    Thread state for a waiting thread with a specified waiting time. A thread is in the timed waiting state due to calling one of the following methods with a specified positive waiting time:
    Thread.sleep
    Object.wait with timeout
    Thread.join with timeout
    LockSupport.parkNanos
    LockSupport.parkUntil

    TIMED_WAITING(限时等待):线程处于限时等待状态,与等待状态不同的是,在指定时间后,线程会被自动唤醒。

    Tips:也有翻译成超时等待的,但是我觉得不太合适。

    再再再写一段代码:

    public class TimedWaitingStateTransition {
        public static void main(String[] args) throws InterruptedException {
            Thread t = new Thread(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程[" + Thread.currentThread().getName() + "]限时等待后,状态:[" + Thread.currentThread().getState() + "]");
            });
            System.out.println("线程[" + t.getName() + "]创建,状态:[" + t.getState() + "]");  
            t.start();
            System.out.println("线程[" + t.getName() + "]启动,状态:[" + t.getState() + "]");  
            TimeUnit.MILLISECONDS.sleep(50);
            System.out.println("线程[" + t.getName() + "]限时等待中,状态:[" + t.getState() + "]");
            TimeUnit.MILLISECONDS.sleep(100);
            System.out.println("线程[" + t.getName() + "]结束,状态:[" + t.getState() + "]");  
        }
    }
    

    线程t创建后,进入NEW状态,启动后进入RUNNABLE状态,线程休眠100ms,进入TIMED_WAITING状态,休眠时间结束后,进入RUNNABLE状态,最后线程执行结束,进入TERMINATED状态。

    限时等待状态的转换:


    限时等待状态的转换.png

    线程状态转换总结

    上面我们通过4段代码了解了线程状态的转换,下面我们通过一张图来总结下线程的状态转换。


    线程状态转换总结.png

    结语

    今天分享了并发编程的统计数据,因此面试题目较少,不过还是希望对你有帮助。

    下一篇内容是剩余的5个知识点(如果一篇能够写完的话):

    • Thread类核心方法
    • 同步与互斥
    • Java线程调度方式
    • 死锁的产生与解决
    • 多线程的优点

    本篇文章的代码仓库:


    好了,今天就到这里了,Bye~~

    相关文章

      网友评论

        本文标题:02.关于线程你必须知道的8个问题(上)

        本文链接:https://www.haomeiwen.com/subject/ewdzzrtx.html