★1. 什么是Java内存模型?(对比Java内存布局)

Java内存模型(JMM)是一种符合内存模型规范,定义了程序中各个变量的访问规则,一般都围绕原子性、可见性、有序性三方面展开。JMM规定所有的变量都存储在主内存中,其中线程均有自己的工作内存区。
其中工作内存区保存着被该线程使用的变量的主内存副本,线程对变量的所有操作都是在该工作空间进行的,不能直接读写主内存数据。操作完成后,线程将工作内存通过缓存一致性协议将数据刷回主内存。

2. 什么是AQS?

Abstract queued Synchronizer,是一个用于构建锁和同步容器的框架。许多concurrent类中许多类,例如ReentrantLock、Semphore等都是基于AQS实现加锁和释放锁的功能
二、线程、多线程机制 - 图1
包含了state变量(代表加锁的状态)、加锁线程(记录当前是哪个线程调用了加锁功能)、等待队列等并发中的核心组件。
例如,线程A调用ReentrantLock.lock()方法,此时没有其他线程进行加锁操作(state变量为0),那么线程1成功加锁(state变量为1),加锁对象的线程指向线程A。
若线程1没有调用unlock()方法释放锁,其他线程想要调用lock()加锁判断state变量是否为0以及当前加锁线程是否为其本身,如若都不满足,则线程2就加锁失败,线程2就会把自身放入AQS的一个等待队列,等待加锁的线程释放锁,然后进行加锁操作。

3. 什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)?

原子操作是指一个不受其他操作影响的操作任务单元。不会被打断地的操作。原子操作是在多线程环境下避免数据不一致必须的手段。
为了保证增加操作是原子的,在JDK1.5之前我们可以使用同步技术来做到这一点。到JDK1.5,java.util.concurrent.atomic包提供了int和long类型的装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。

9. 什么是Executors框架?

无限制的线程创建会导致内存的溢出,通过Executors框架可以创建出很方便的创建出一个线程池:

  1. - **newCachedThreadPool**创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  2. - **newFixedThreadPool** 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  3. - **newScheduledThreadPool** 创建一个定长线程池,支持定时及周期性任务执行。
  4. - **newSingleThreadExecutor** 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

10. 什么是阻塞队列?如何使用阻塞队列来实现生产者-消费者模型?

阻塞队列的概念:
阻塞队列是指一个支持附加操作的队列,
在队列为空时,获取元素的线程会等待队列变为非空。
当队列满时,存储元素的线程会等待队列可用。
应用场景
生产者往队列里面添加元素的线程,而消费者是往队列里面拿元素的线程,基于线程的通信,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。
BlockingQueue是一种数据结构,支持一个线程往里存资源,一个线程往外取资源

11. 什么是Callable和Future?

当我们需要创建线程时,可以采用实现Callable接口并重写call方法来完成。
当我们需要获取线程结果时,Calllabale一般用于产生线程的结果,Future一般用于获取线程的结果
一般我们使用callable创建线程时,Executors类提供了一些方法在线程池中执行Callable内的任务,但由于Callable是并行的,需要等待结果的返回 ,Future类很好的解决了这个问题。
在线程池提交Callable任务后返回了一个Future对象,使用它可以知道Callable任务的状态和得到Callable返回的执行结果

12. 什么是FutureTask?

基本概念:
FutureTask可用于异步获取执行结果或取消执行任务的场景。通过传入Runnable或者Callable的任务给FutureTask,直接调用其run方法或者放入线程池执行,之后可以在外部通过FutureTask的get方法异步获取执行结果,因此,FutureTask非常适合用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
另外,FutureTask还可以确保即使调用了多次run方法,它都只会执行一次Runnable或者Callable任务,或者通过cancel取消FutureTask的执行等。
应用场景:
(1)利用FutureTask和ExecutorService,可以用多线程的方式提交计算任务,主线程继续执行其他任务,当主线程需要子线程的计算结果时,在异步获取子线程的执行结果。
(2)FutureTask在高并发环境下确保任务只执行一次

13. 什么是同步容器和并发容器的实现?

16. ThreadLocal的设计理念与作用?

原理分析:
Java中的ThreadLocal类允许我们创建只能被同一个线程读写的变量。ThreadLocal内部有一个内部类ThreadLocalMap,其key值为ThreadLocal对象,value为对应的值。因此,如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码,它们也无法访问到对方的ThreadLocal变量
若多个线程读取ThreadLocal的对象,每个线程只能读取自身创建的私有的ThreadLocal变量设置的值,因此不会存在线程安全问题。
使用的场景:
1、进行对象的跨层传递时,使用ThreadLocal可以避免多次传递。
2、进行事务操作时,可以存储线程事务信息。
3、Spring框架JDBC Connection是放在ThreadLocal中。
ThradLocal内存泄漏的原因:
内存泄漏:JVM中不再使用的对象或者变量占用的内存不能被回收,导致内存泄露。
每一个Thread维护了一个ThreadLocalMap对象,其中map的key为弱引用,而value为强引用。因此一个ThreadLocal没有外部强引用时,其key势必会被GC回收,但ThreadLocalMap的生命周期和Thread生命周期一样长,只要Thread还在,其map的value值还存在,导致内存泄露。
正确的方法:
1、每次使用完ThreadLocal,都调用remove()方法清除数据
2、将ThreadLocal的变量定义为private static ,保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除。

18. Concurrent包里的其他东西:ArrayBlockingQueue、CountDownLatch等等。

ArrayBlockingQueue
此队列按照先进先出(FIFO)的原则对元素进行排序,但是默认情况下不保证线程公平的访问队列,即如果队列满了,那么被阻塞在外面的线程对队列访问的顺序是不能保证线程公平(即先阻塞,先插入)的。
CountDownLatch
CountDownLatch 允许一个或多个线程等待其他线程完成操作。

19. synchronized和ReentrantLock的区别?

基本概念

  - **可重复入锁**
  - **中断锁**
  - **公平锁和非公平锁**
  - **CAS操作**

20. Semaphore有什么作用?

  • semphore通过维护若干个许可证来控制线程对共享资源的访问。 如果许可证剩余数量大于零时,线程则允许访问该共享资源;如果许可证剩余数量为零时,则拒绝线程访问该共享资源。 Semaphore所维护的许可证数量就是允许访问共享资源的最大线程数量。 所以,线程想要访问共享资源必须从Semaphore中获取到许可证。
  • semphore有两个核心方法:
    • acquire():从信号量中获取一个许可,进入阻塞状态。
    • release():释放持有许可证的线程;
  • 然而,实际上并没有真实的许可证对象供线程使用,Semaphore只是对可用的数量进行管理维护。
  • 底层原理:

    • Semaphore内部主要通过AQS(Abstract Queued Synchronizer)实现线程的管理。Semaphore在构造时,需要传入许可证的数量,它最后传递给了AQS的state值。线程在调用acquire方法获取许可证时,如果Semaphore中许可证的数量大于0,许可证的数量就减1,线程继续运行,当线程运行结束调用release方法时释放许可证时,许可证的数量就加1。如果获取许可证时,Semaphore中许可证的数量为0,则获取失败,线程进入AQS的等待队列中,等待被其它释放许可证的线程唤醒。
    • 总结:如果线程要访问一个资源就必须先获得信号量。如果信号量内部计数器大于0,信号量减1,然后允许共享这个资源;否则,如果信号量的计数器等于0,信号量将会把线程置入休眠直至计数器大于0.当信号量使用完时,必须释放
    • 注:Semphore底层有两个构造函数

      21. Java Concurrency API中的Lock接口(Lock interface)是什么?对比同步它有什么优势?

      Lock接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。
      它的优势有:

    • 可以使锁更公平。

    • 可以使线程在等待锁的时候响应中断。
    • 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间。
    • 可以在不同的范围,以不同的顺序获取和释放锁。

      22. Hashtable的size()方法中明明只有一条语句”return count”,为什么还要做同步?

      固定类的同步方法,同一时间只能有一个线程执行。
      对于固定类的非同步方法,可能会有多个线程同时进行访问,此时该方法是不安全的:如果线程A访问Hashtable类向其添加数据,同时线程B访问Hashtable类读取size()方法。
      此时可能出现的问题为:线程A已经添加完数据后,size还没有++;而线程B读取的size()方法是还没有size没有++的数据,导致数据读取的不正确。

      23. ConcurrentHashMap的并发度是什么?

      ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势。

      24. ReentrantReadWriteLock读写锁的使用?

25. CyclicBarrier和CountDownLatch的用法及区别?

相同点:
CountDownLatch和CyclicBarrier都有让多个线程等待同步然后再开始下一步动作的意思,等待其他线程完成操作
区别:
CountDownLatch : 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。
CyclicBarrier : N个线程相互等待,任何一个线程没有到达或完成时,所有的线程都必须互相等待。
这样应该就清楚一点了,对于CountDownLatch来说,重点是那个“一个线程”, 是它在等待, 而另外那N的线程在把“某个事情”做完之后可以继续等待,可以终止。
而对于CyclicBarrier来说,重点是那N个线程,他们之间任何一个没有完成,所有的线程都必须等待.
二、线程、多线程机制 - 图2

26. LockSupport工具?

  • LockSupport核心类的方法就两个:
    • part() :使当前线程阻塞,
    • unpark():唤醒指定线程进行调用
  • LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,可以把许可看成是一种(0,1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是1。初始时,permit为0,当调用unpark()方法时,线程的permit加1,当调用park()方法时,如果permit为0,则调用线程进入阻塞状态。

    27. Condition接口及其实现原理?

    Condition是一个多线程间协调通信的工具类。Condition作为一个条件类,很好的自己维护了一个等待信号的队列,并在适时的时候将结点加入到AQS的等待队列中来实现的唤醒操作。
    线程1调用ReentrantLock.lock()方法,线程加入AQS等待队列。
    调用线程1中Condition类的await()方法,该线程从AQS中移除,对应是锁的释放。
    然后马上加入到Condition类的等待队列中,此时需要signal()方法来唤醒该线程。
    线程2因为线程1锁的释放,判断可以获取锁,此时线程2获取锁,加入AQS等待队列。
    线程2调用Condition类的signal()方法,这时候Condition等待队列中只有线程1,于是线程1被取出来加入到AQS的等待队列中,但此时线程1并没有被真正唤醒。
    线程2调用ReentrantLock.unlock()方法,释放锁,此时因为AQS等待队列中只有线程1,此时AQS释放锁后从头到尾按顺序唤醒线程时,线程1被唤醒,然后线程1开始执行。
    直至线程1调用ReentrantLock.unlock()方法,整个过程执行完毕。

    28. Fork/Join框架的理解?

    29. wait()和sleep()的区别?

    1) 原理不同
    sleep( ) 是Thread类的静态方法,它的功能是使线程暂停一段时间,一段时间后线程会自动苏醒;
    wait () 是Object类的方法,它的功能是使线程暂停执行,需要notify() 或者是notifyAll()方法来唤醒;
    2)对锁的机制不同
    sleep()不涉及线程之间的通信问题,因此不会释放锁;
    wait()方法会使当前线程释放掉所占用的锁,供其他线程使用 ;
    3)使用区域不同
    sleep()可以放在任何地方使用,但是必须捕获异常;
    wait()方法只能在同步方法或者同步代码块中使用,一般配合synchronized使用;

    31. start()方法和run()方法的区别?

    线程的生命周期,当一个线程调用了start()方法,该线程进入就绪状态,当获得cpu调度时,才会进行运行状态,执线程内部的run()方法。
    一个线程调用start()方法以后,并不会马上执行,而是进入就绪状态,被放入等待队列,等待调度。一旦该线程获得了CPU调度执行,那么该线程会通过JVN内存,调用线程内部的run()方法,执行线程体。

    32. Runnable接口和Callable接口的区别?

    1)重写的方法不同
    Runnable接口重写的run()方法返回值类型为void,单纯执行线程执行体;
    Callable接口重写的run()方法具有返回值,是一个泛型,可以结合FutureTask和Future获取执行的结果;
    2)抛出异常值
    Callable接口可以抛出异常值,而Runnable接口不可以抛出异常值。

    33. volatile关键字的作用?

    volatile是java线程提供的一种轻量级同步机制,基于 volatile 关键字修饰时多线程的共享变量来实现线程间相互通信是使用共享内存的思想,大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。
    (1)原子性: 即一个CPU调度操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
    (2)可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
    (3)有序性:即程序执行的顺序按照代码的先后顺序执行。

总结:volatile关键词保证了可见性,但不保证原子性!

34. Java中如何获取到线程dump文件?

基本概念:
线程dump也就是线程堆栈!
获取方法
(1)取到线程的pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef | grep java
(2)打印线程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid
另外提一点,Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈

37. 高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?

1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换

2)并发不高、任务执行时间长的业务要区分开看:

a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务

b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换

3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考其他有关线程池的文章。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

38. 如果你提交任务时,线程池队列已满,这时会发生什么?

(1)如果当前线程池队列使用的无界队列LinkedBlockingQueue,那么继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务;
(2)如果当前线程池队列使用的是有界队列如ArrayListBlockingQueue,任务首先还是会被添加到ArrayListBlockingQueue中,当前队列会根据线程数是否超过了最大线程数maximunPoolSize,如果超过了最大线程数maximunPoolSize,则会使用拒绝策略处理ArrayListBlockingQueue中的任务。

41. 并发编程(concurrency)并行编程(parallellism)有什么区别?

  • 并行是指两个或多个事件在同一时刻发生,并发是指两个或多个事件在同一时间间隔内发生!
  • 并行是指不同实体上的两个或多个事件,并发是指同一实体上的多个事件!
  • 并行是指在多台处理器上同时处理多个任务,并发是指在同台处理器上处理多个任务!

注:并发是因为多进程/多线程都是需要去完成的任务,不并行是因为并行与否由操作系统的调度器决定,可能会让多个进程/线程被调度到同一个CPU核心上。只不过调度算法会尽量让不同进程/线程使用不同的CPU核心,所以在实际使用中几乎总是会并行,但却不能以100%的角度去保证会并行。也就是说,并行与否程序员无法控制,只能让操作系统决定。
再次注明,并发是一种现象,之所以能有这种现象的存在,和CPU的多少无关,而是和进程调度以及上下文切换有关的。
并行是通过操作系统决定,而并发是通过进程来调度的!!!

44. 如何在两个线程之间共享数据?

Java 内存模型(JMM)解决了可见性和有序性的问题,而锁解决了原子性的问题。
method1:将需要操作的数据封装成类,并创建操作这些数据的方法,并利用synchronized对这些方法加锁;
method2:将Runnable对象作为类的内部类,需要操作的数据作为类的成员变量,每个线程对数据操作方法定义在外部类,以实现数据的各个操作的同步与互斥;

45. 生产者消费者模型的作用是什么?

生产者消费者问题就是要控制线程的阻塞状态,保证生产者和消费者进程在一定条件下,一直稳定运行.
其实质在于使用阻塞队列保证线程生产数据和消费数据的强耦合,生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

46. 怎么唤醒一个阻塞的线程?

1、synchronized的wait() 和notify()/notiftAll()
2、ReentrantLock创建的Condition可重入锁调用await()和signal()/signalAll()方法
3、LockSupport的park和unpark()实现线程的阻塞和唤醒;
注:ReentrantLock底层是基于AQS实现的,而AQS的底层是基于LockSupport实现的,所以LockSupport的效率相比更高!

48. 单例模式的线程安全性?

饿汉式(程序一开始就创建实例化对象):线程安全
懒汉式(只有等需要使用时才创建实例化对象):线程不安全—>
懒汉式线程不安全解决方案:
1、使用同步机制synchronized关键词修饰实例化方法
2、创建内部类
3、添加关键字volatile 修饰成员变量

49. 线程类的构造方法、静态块是被哪个线程调用的?

线程类的构造方法、静态块是被new 这个线程类的对象所调用的:
例如Thread2.new Thread(thread1),那么线程1的构造方法、静态块是由线程2所调用的,而线程1内部的run()方法执行体是由其自身所调用;
main函数中new了Thread2,那么线程2的构造方法、静态块是由main主线程所调用的,内部的run()方法是尤其自身所调用

50. 同步方法和同步块,哪个是更好的选择?

遵循一条原则:同步的范围越小越好!

同步块,意味着同步块之外的是异步执行的,这样效率更高!而且同步块不会锁住整个对象,而同步方法会锁着整个对象。

★51. 如何检测死锁?怎么预防死锁?

(1)死锁产生的四大必要条件:
1、互斥性:即当资源被一个线程使用时,其他线程不得使用
2、不可抢占:资源请求者不能强制从资源占有着从获取资源,只能等资源占有着主动释放
3、请求和保持:当资源请求这对资源请求的同时保持对原有资源的占用
4、循环等待:即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
(2)死锁的根本原因
1)是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环;
2)默认的锁申请操作是阻塞的。所以要避免死锁,就要在一遇到多个对象锁交叉的情况,就要仔细审查这几个对象的类中的所有方法,是否存在着导致锁依赖的环路的可能性。要采取各种方法来杜绝这种可能性。
(3)如何避免死锁
预防死锁:
1、资源一次性分配:一次性分配完所以的资源,这样就不会再有进程请求获取资源了
2、一个资源也不分配,就是给一个进程分配资源时,如果无法给该进程分配它所需要的所有资源,则直接一个资源也不分配
3、资源有序分配:让系统给每类资源都赋予一个编号,每一个进程按编号递增的顺序请求资源
避免死锁:
1、银行家算法:允许进程动态地申请资源。因而,系统在进行资源分配之前,预先估算一下资源分配的安全性。如果发现此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待。
如何判断资源分配的安全性:即在分配过程中,不会出现某一进程后续需要的资源量 比 其他所有进程及当前剩余资源量总和 还大的情况。
解除死锁:
当发现有进程死锁后,应立即把它从死锁状态中解脱出来
1、给予资源,从其他进程剥夺足够数量的资源,给与死锁进程,以解除它的死锁状态
2、直接撤销进程:可以直接撤销死锁进程或者撤销代价最小的进程,直至有足够的资源可用。
(4)死锁的检测:
1、jstack命令
jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。
Jstack工具可以用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
2、jConsole工具
Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。
如何预防死锁
1、以确定的顺序获得锁:如果必须获取多个锁,那么在设计的时候需要充分考虑不同线程之前获得锁的顺序。
2、超时放弃:当使用synchronized关键词提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。