1、请写出4种创建线程的方式

继承Thread类、实现Runnable接口、实现callable接口、使用线程池

2、聊聊线程的生命周期

  1. NEW(线程刚创建,还未调用start方法)、RUNNABLE(线程正在运行或正在等待分配资源)、BLOCKED(线程等待获取监视器锁用于进入或者重新进入同步代码块或方法)、WAITING(线程一直等待,直到其他线程执行唤醒操作)、TIMED_WAITING(线程在特定时间内一直等待,直到超时或者其他线程执行唤醒操作)和TERMINATED(线程已经执行完成),这6个状态就贯穿在线程的整个生命周期当中。
    1、当通过Thread类的构造函数生成Thread实例时,此时线程处于NEW状态。
    2、当调用start()方法时,此时线程处于RUNNABLE状态,在RUNNABLE状态中又有准备就绪和运行中两种状态。
    3、当处于RUNNABLE状态的线程调用同步方法或同步代码块时,若此时其他线程已经获取了锁,那么该线程转换为BLOCKED状态。当其他线程释放锁,该线程获取到锁时,此时线程从BLOCKED状态转换为RUNNABLE状态。
    4、当处于RUNNABLE状态的线程调用join()、wait()或LockSupport.park()方法时,线程转换为WAITING状态,当有其他线程执行notify()、notifyAll()、LockSupport.unpark(Thread)方法时,线程又从WAITING转换为RUNNABLE状态。
    5、当处于RUNNABLE状态的线程调用join(long)、wait(long)、LockSupport.parkNanos(long)、LockSupport.parkUntil(long)和Thread.sleep(long)方法时,线程转换为TIMED_WAITING状态,当有其他线程执行notify()、notifyAll()、LockSupport.unpark(Thread)或等待时间已经超过超时时间,线程又从TIMED_WATING状态转换为RUANNBLE状态。
    6、当线程run方法的逻辑都执行完成后,线程转换到TERMINATED状态
  2. JUC - 图1
  3. JUC - 图2

    3、谈谈sleep和wait的区别

  4. 首先他们来自不同的类:sleep来自Thread类,和wait来自Object类
    有没有释放锁(释放资源):sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
    使用范围不同:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用.
    sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常

    4、线程调度方法:join、notify

    5、线程池的五大状态

    JUC - 图3
    1、RUNNING
    (1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
    (2) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!
    2、 SHUTDOWN
    (1) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
    (2) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
    3、STOP
    (1) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
    (2) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
    4、TIDYING
    (1) 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
    (2) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
    当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
    5、 TERMINATED
    (1) 状态说明:线程池彻底终止,就变成TERMINATED状态。
    (2) 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。

    6、线程池的七大参数:核心线程,阻塞队列,最大线程,拒接策略

    一、corePoolSize 线程池核心线程大小
    线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。
    二、maximumPoolSize 线程池最大线程数量
    一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接将任务交给这个空闲线程来执行,如果没有则会缓存到工作队列(后面会介绍)中,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。
    三、keepAliveTime 空闲线程存活时间
    一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定
    四、unit 空闲线程存活时间单位
    keepAliveTime的计量单位
    五、workQueue 工作队列
    新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
    ①ArrayBlockingQueue
    基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
    ②LinkedBlockingQuene
    基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
    ③SynchronousQuene
    一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
    ④PriorityBlockingQueue
    具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
    六、threadFactory 线程工厂
    创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
    七、handler 拒绝策略
    当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:
    ①CallerRunsPolicy
    该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
    ②AbortPolicy
    该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
    ③DiscardPolicy
    该策略下,直接丢弃任务,什么都不做。
    ④DiscardOldestPolicy
    该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列

    7、线程池的工作原理

    image.png

    8、线程池的线程增长和回收策略

    增长策略:默认线程池接收到任务,创建一个线程去执行当前任务,当线程数大于核心线程数,会将任务添加到任务队列中,当队列满了,会创建新的线程去
    执行任务。当线程数大于最大线程数停止。并启动拒绝策略。
    回收策略:线程池中线程的数量大于核心线程数量&&有空闲线程&&空闲线程的空闲时间大于了KeepAliveTime时,会对空闲线程进行回收,直到等于核心线程为止。

    9、锁的相关分类:乐观|悲观、公平|非公平、可重入|不可重入、共享|排他

    1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不
    总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变
    量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
    2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争
    总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二
    十一,直接上了锁就操作资源了。
    公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平
    锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁
    表现的性能不是很好。

    10、对象的锁升级过程(无锁、偏向锁、轻量级锁、重量级锁)

    image.png
    无锁:刚new出来的对象,无任何锁竞争
    偏向锁:一段同步代码一直被一个线程所访问,对象会在对象头上标识该线程,那么该线程会自动获取到锁
    自旋锁:多个线程竞争同步代码,会以自旋和CAS的方式获取对象的锁
    重量级锁:CAS竞争激烈,升级到操作系统的申请锁,由操作系统决定是否获取到锁

    11、synchronized的三种用法和区别

  • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

    12、synchronized的实现原理

    1、synchronznized映射成字节码指令就是增加两个指令:monitorenter、monitorexit,
    当一条线程执行时遇到monitorenter指令时,它会尝试去获得锁,如果获得锁,那么所计数器+1(为什么要加1因为它是可重入锁,可根据这个琐计数器判断锁状态),如果没有获得锁,那么阻塞,当它遇到一个monitoerexit时,琐计数器会-1,当计数器为0时,就释放锁
    (tips:节码中出现的两个monitoerexit指令的原因是:一个正常执行-1,令一个异常时执行,这两个用goto的方式只执行一个)
    2、Lock底层则基于volatile和cas实现

    13、synchronized和Lock(JUC)区别

  1. 来源: lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;
  2. 异常是否释放锁:
    synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)
  3. 是否响应中断
    lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;
  4. 是否知道获取锁 :Lock可以通过trylock来知道有没有获取锁,而synchronized不能;
  5. Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)
  6. 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量 线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
  7. synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度,

    14、CAS(三个问题)、ABA、自旋、原子操作

    15、AQS的原理和资源的共享方式(独占、共享)

    16、死锁的原因,如何避免?

    死锁是指两个以上的线程永远阻塞的情况,这种情况产生至少需要两个以上的线程和两个以上的资
    源。
    分析死锁,我们需要查看Java应用程序的线程转储。我们需要找出那些状态为BLOCKED的线程和他
    们等待的资源。每个资源都有一个唯一的id,用这个id我们可以找出哪些线程已经拥有了它的对象锁。
    避免嵌套锁,只在需要的地方使用锁和避免无限期等待是避免死锁的通常办法。

    17、volatile的作用和原理

    1)多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线
    程之间的可见性,即每次读取到volatile变量,一定是最新的数据。
    2)代码底层执行不像我们看到的高级语言——Java程序这么简单,它的执行是Java代码—>字节码—>根据
    字节码执行对应的C/C++代码—>C/C++代码被编译成汇编语言—>和硬件电路交互,现实中,为了获取更
    好的性能JVM可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用volatile则会对
    禁止语义重排序,当然这也一定程度上降低了代码执行效率。

    18、CountDownLatch(闭锁)

    CountDownLatch简单的说就是⼀个线程等待,直到他所等待的其他线程都执⾏完成并且 调⽤
    countDown()⽅法发出通知后,当前线程才可以继续执⾏。

    19、Samaphore(信号量)

    Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可
    以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某
    个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int
    型整数n=1,相当于变成了一个synchronized了。

    20、ThreadLocal作用,内存溢出?

    ThreadLocal可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部
    副本变量就行了,做到了线程之间互相隔离,相比于synchronized的做法是用空间来换时间。
    ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组,
    Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key value键值对
    的能力。
    弱引用的目的是为了防止内存泄露,如果是强引用那么ThreadLocal对象除非线程结束否则始终无
    法被回收,弱引用则会在下一次GC的时候被回收。 但是这样还是会存在内存泄露的问题,假如key和ThreadLocal对象被回收之后,entry中就存在key 为null,但是value有值的entry对象,但是永远没办法被访问到,同样除非线程结束运行。 但是只要ThreadLocal使用恰当,在使用完之后调用remove方法删除Entry对象,实际上是不会出
    现这个问题的。
    image.png

    21、CyclicBarrier

    类似于闭锁,它可以阻塞⼀组线程,只有所有线程全部到达以后,才能够继续 执⾏,so
    线程必须相互等待。这在并⾏计算中是很有⽤的,将⼀个问题拆分为多个独⽴的⼦问 题,当线程到达栅
    栏时,调⽤await等待,⼀直阻塞到所有参与线程全部到达,再执⾏下⼀步任务。

    22、原子类

    JUC - 图7
    1.AtomicInteger常用方法
    get:获取当前的值
    getAndSet:获取当前的值,并设置新的值
    getAndIncrement:获取当前的值,并自增
    getAndDecremrnt:获取当前值,并自减
    getAndAdd(n):获取当前的值,并加上预期的值
    boolean compareAndSet(sepect,update):如果输入的等于预期值,则以原子方式将该值设置为输入值