面试中遇到3个字--“多线程”

不知道大家有没有面试的时候,被考官通过“开放式”的提问方式问过一些一脸萌B或者不知所措的问题。

what the ****

最近面试一家公司,那个技术主管问了我一个问题  "请你说说多线程?" ,这个7文字让我不知所措,发呆了几秒钟,这几秒钟在想 “ 在开玩笑吗?、我该从什么方面回答呢?、说些什么才让面试官觉得可以呢?面试官想知道那些内容呢?我从那些技术点开始说呢? ”  反正就愣住了~~有一种硬生生扇了一把然后却不知道发什么事的感觉。当然这篇文章的重点是想梳理一下“ 多线程 ”相关的内容,面试结果怎样大家肯定知道了

多线程相关知识

以下是我梳理了一下多线程不同维度的相关知识点:

多线程思维导图

1)生命周期:

            线程也是有从产生到死亡的过程,其中包含了“创建”、“就绪”、“运行”、“阻塞”、“死亡”,这5个步骤体现了线程各种不同的状态,下图就是现实一下线程完整的生命流程:

扫描二维码关注公众号,回复: 3396981 查看本文章

线程生命周期

    新建状态:

    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

    就绪状态:

    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

    运行状态:

    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就   绪状态和死亡状态。

    阻塞状态:

    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或   获得设备资源后可以重新进入就绪状态。可以分为三种:

    等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。

    同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

    其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

    死亡状态:

    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。


2)创建方式:

                1. 通过实现 Runnable 接口;

                实现Runnable类,然后调用通过 new Thread(Runnable run,String threadname) 构造函数创建。

                2.通过继承 Thread 类本身;

                 Thread thread = new Thread();

                3.通过 Callable Future 创建线程。

1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。

FutureTask ft = new FutureTask<>(Callable<T> call);

Thread thread = new Thread(ft,"有返回值的线程");

thread.start();

T  t = ft.get();

3)运行方式:

          多线程包含运行或者执行方式 “并发”“并行”刚开始的时候经常把这2个方式混淆,通过多次的查看资料,总结了自己的看法,先上一张图,如下:

    Erlang 之父 Joe Armstrong 用一张5岁小孩都能看懂的图解释了并发并行的区别。图的上半部分就是并发:两个队列争交替使用一台coffee机;图的下半部分就是并行:两个队列同时使用两台coffee机。

    从图中,我们还可以看出“并发” 与 “并行” 两者并非有互斥的概率,而是可以独立或者同时存在的,比如说:两台coffee机也可各自拥有两个队列在交替使用。

    总结:并发是多个线程被(一个)cpu 轮流切换着执行,并行是多个线程被多个cpu同时执行。

4)线程池:

    下面我从几个问题论述 “线程池” 相关知识:

     1、为什么要用线程池?

                在平常写代码过程中,我们使用比较频繁就是并发编程,以前为了方便都是直接new Thread(),然后发现项目的不断迭代,业务逻辑日益繁重,导致代码结构的越来越复杂而且每次创建线程也会存在开销(创建与销毁)的问题,如果没有处理好它们的生命周期就比较容易出现内存溢出等相关性能问题。

                所以使用线程池就可以达到线程复用管理生命周期,解决每次new Thread的内存开销。

     2、线程池(ThreadPoolExecutor)简单介绍

            java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,因此如果要透彻地了解Java中的线程池,必须先了解这个类。下面我们来看一下ThreadPoolExecutor类的具体实现源码。

public class ThreadPoolExecutor extends AbstractExecutorService {

    .....

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,

            BlockingQueue workQueue);

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,

            BlockingQueue workQueue,ThreadFactory threadFactory);

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,

            BlockingQueue workQueue,RejectedExecutionHandler handler);

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,

        BlockingQueue workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);

    ...

}

corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;

maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;

keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;

unit:TimeUnit.DAYS; //天TimeUnit.HOURS; //小时TimeUnit.MINUTES; //分钟TimeUnit.SECONDS; //秒TimeUnit.MILLISECONDS; //毫秒TimeUnit.MICROSECONDS; //微妙TimeUnit.NANOSECONDS; //纳秒

workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:ArrayBlockingQueue;LinkedBlockingQueue;SynchronousQueue;

threadFactory:线程工厂,主要用来创建线程。

handler:表示当拒绝处理任务时的策略,有以下四种取值:ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。

         下面是常用的ThreadPoolExecutor的关系。

类关系图

              下面是工作流程图:

     3、线程池有些?

            1.可缓存线程池CachedThreadPool()

        public static ExecutorService newCachedThreadPool() {                                                                return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,                                                                     new SynchronousQueue()); }  

这种线程池内部没有核心线程,线程的数量是有没限制的。在创建任务时,若有空闲的线程时则复用空闲的线程,若没有则新建线程。没有工作的线程(闲置状态)在超过了60S还不做事,就会销毁。

             2.FixedThreadPool()定长线程池

        public static ExecutorService newFixedThreadPool(intnThreads){                                                   return new ThreadPoolExecutor(nThreads, nThreads,0L,                                                                                                                       TimeUnit.MILLISECONDS,new LinkedBlockingQueue());}

该线程池的最大线程数等于核心线程数,所以在默认情况下,该线程池的线程不会因为闲置状态超时而被销毁。如果当前线程数小于核心线程数,并且也有闲置线程的时候提交了任务,这时也不会去复用之前的闲置线程,会创建新的线程去执行任务。如果当前执行任务数大于了核心线程数,大于的部分就会进入队列等待。等着有闲置的线程来执行这个任务 。

              3.SingleThreadPool()

           public static ExecutorService newSingleThreadExecutor(){return new FinalizableDelegatedExecutorService (newThreadPoolExecutor(1,1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue()));}

有且仅有一个工作线程执行任务所有任务按照指定顺序执行,即遵循队列的入队出队规则

                4.ScheduledThreadPool()

            public static ScheduledExecutor ServicenewScheduledThreadPool(intcorePoolSize){return new ScheduledThreadPoolExecutor(corePoolSize);}

            public ScheduledThreadPoolExecutor(intcorePoolSize){                                super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,new DelayedWorkQueue());}

DEFAULT_KEEPALIVE_MILLIS就是默认10L,这里就是10秒。这个线程池有点像是吧CachedThreadPool和FixedThreadPool 结合了一下。不仅设置了核心线程数,最大线程数也是Integer.MAX_VALUE。这个线程池是上述4个中为唯一个有延迟执行和周期执行任务的线程池作者:我弟是个程序员链接:https://www.jianshu.com/p/ae67972d1156來源:简书简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

     4、线程池使用过程需要注意什么?   

    虽然使用线程池能很大程度先提高应用程序的执行性能,但它也有存在相关多线程并发的问题 :   竞争死锁、系统资源不足、线程泄漏、并发错误、任务过载。 

 

5)多线程中三大特征:

     1.可见性

        当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

   若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程2没看到这就是可见性问题。 (Java中可以通过Volatile来修饰变量确保的可见性)

      2.原子性

        即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

     一个很经典的例子就是银行账户转账问题:  比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。这2个操作必须要具备原子性才能保证不出现一些意外的问题。  我们操作数据也是如此,比如i = i+1;其中就包括,读取i的值,计算i,写入i。这行代码在java中是不具备原子性的,则多线程运行肯定会出问题,所以也需要我们使用同步和lock这些东西来确保这个特性了。

     3.有序性

        程序执行的顺序按照代码的先后顺序执行。

        一般来说处理器(JVM)为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。如下:int a = 10;    //语句1int r = 2;    //语句2a = a + 3;    //语句3r = a*a;     //语句4则因为重排序,他还可能执行顺序为 2-1-3-4,1-3-2-4但绝不可能 2-1-4-3,因为这打破了依赖关系。显然重排序对单线程运行是不会有任何问题,而多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。

6)线程间通信:

        在日常开发中,我们会很多时候使用多线程来执行一些耗时的任务,当出现一个需求:线程A、B已经调用start,线程B需要等待线程A完成才能继续执行(常见的应用场景:数据生产/收集与数据消费,往往数据消费存在较多的逻辑处理或者算法,所以为了提高数据生产/收集的效率,会把数据消费与生产的线程独立。),这时候就需要用到线程间通讯。

        线程间通讯的几种方式:

        1.synchronized   

            synchronized包含2种类型锁:

                1)对象锁:非静态方法this已经初始化的对象

                2)类锁:静态方法Object.class

        2.wait/notify

            Object ob = new Object();    ob.wait(); ob.notify();

        3.ReentantLock

            Lock lock=new ReentrantLock(true);//公平锁                                                                                          

           Lock lock=new ReentrantLock(false);//非公平锁                                                                                                                        公平锁指的是线程获取锁的顺序是按照加锁顺序来的,而非公平锁指的是抢锁机制,先lock的线程不一定先获得锁

getHoldCount() 查询当前线程保持此锁的次数,也就是执行此线程执行lock方法的次数

getQueueLength()返回正等待获取此锁的线程估计数,比如启动10个线程,1个线程获得锁,此时返回的是9

getWaitQueueLength(Condition condition)返回等待与此锁相关的给定条件的线程估计数。比如10个线程,用同一个condition对象,并且此时这10个线程都执行了condition对象的await方法,那么此时执行此方法返回10

hasWaiters(Condition condition)查询是否有线程等待与此锁有关的给定条件(condition),对于指定contidion对象,有多少线程执行了condition.await方法

hasQueuedThread(Thread thread)查询给定线程是否等待获取此锁

hasQueuedThreads()是否有线程等待此锁

isFair()该锁是否公平锁

isHeldByCurrentThread() 当前线程是否保持锁锁定,线程的执行lock方法的前后分别是false和true

isLock()此锁是否有任意线程占用

lockInterruptibly()如果当前线程未被中断,获取锁

tryLock()尝试获得锁,仅在调用时锁未被线程占用,获得锁

tryLock(long timeout TimeUnit unit)如果锁在给定等待时间内没有被另一个线程保持,则获取该锁

 4.PipedInputStream 这里不详细介绍

猜你喜欢

转载自blog.csdn.net/qwe851023/article/details/81989826