1 、锁的优化机制了解吗


从 JDK1.6 版本之后, synchronized 本身也在不断优化锁的机制,有些情况下他并不会是一个很重量


级的锁了。优化机制包括自适应锁、自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。


锁的状态从低到高依次为 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 ,升级的过程就是从低到高,降级在


一定条件也是有可能发生的。


自旋锁 :由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起


线程,用户态和内核态的来回上下文切换严重影响性能。自旋的概念就是让线程执行一个忙循环,


可以理解为就是啥也不干,防止从用户态转入内核态,自旋锁可以通过设置 -XX:+UseSpining 来开


启,自旋的默认次数是 10 次,可以使用 -XX:PreBlockSpin 设置。


自适应锁 :自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上


的自旋时间和锁的持有者状态来决定。


锁消除 :锁消除指的是 JVM 检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要


加锁,就会进行锁消除。


锁粗化 :锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操


作序列之外。


阿里内部资料 偏向锁 :当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程 ID ,之后


这个线程再次进入同步块时都不需要 CAS 来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线


程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他


线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。可以用过设置 -XX:+UseBiasedLocking 开


启偏向锁。


轻量级锁 : JVM 的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候, JVM 将会使用


CAS 方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,


当前线程就尝试自旋来获得锁。


整个锁升级的过程非常复杂,我尽力去除一些无用的环节,简单来描述整个升级的机制。


简单点说,偏向锁就是通过对象头的偏向线程 ID 来对比,甚至都不需要 CAS 了,而轻量级锁主要就


是通过 CAS 修改对象头锁记录和自旋来实现,重量级锁则是除了拥有锁的线程其他全部阻塞。


2 、说说进程和线程的区别?


1. 进程是一个 “ 执行中的程序 ” ,是系统进行资源分配和调度的一个独立单位。


2. 线程是进程的一个实体,一个进程中拥有多个线程,线程之间共享地址空间和其它资源(所以


通信和同步等操作线程比进程更加容易)


3. 线程上下文的切换比进程上下文切换要快很多。


( 1 )进程切换时,涉及到当前进程的 CPU 环境的保存和新被调度运行进程的 CPU 环境的设置。


( 2 )线程切换仅需要保存和设置少量的寄存器内容,不涉及存储管理方面的操作。


阿里内部资料 25 ,产生死锁的四个必要条件?


1. 互斥条件:一个资源每次只能被一个线程使用


2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放


3. 不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺


4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系


3 、如何避免死锁?


指定获取锁的顺序,举例如下:


1. 比如某个线程只有获得 A 锁和 B 锁才能对某资源进行操作,在多线程条件下,如何避免死锁?


2. 获得锁的顺序是一定的,比如规定,只有获得 A 锁的线程才有资格获取 B 锁,按顺序获取锁就可


以避免死锁!!!


4 ,线程池核心线程数怎么设置呢?


分为 CPU 密集型和 IO 密集型


CPU


这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N ( CPU 核心数) +1 ,比 CPU 核心数多出


来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦


任务暂停, CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空


闲时间。


IO 密集型


这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占


用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们


可以多配置一些线程,具体的计算方法是 :


核心线程数 =CPU 核心数量 *2 。


5 Java 线程池中队列常用类型有哪些?


ArrayBlockingQueue 是一个基于数组结构的 有界阻塞队列 ,此队列按 FIFO (先进先出)原则


对元素进行排序。


LinkedBlockingQueue 一个基于链表结构的 阻塞队列 ,此队列按 FIFO (先进先出)


排序元


素,吞吐量通常要高于 ArrayBlockingQueue 。


SynchronousQueue 一个不存储元素的 阻塞队列


PriorityBlockingQueue 一个具有优先级的 无限阻塞队列 。 PriorityBlockingQueue 也是 基于


最小二叉堆实现


DelayQueue


阿里内部资料 只有当其指定的延迟时间到了,才能够从队列中获取到该元素。


DelayQueue 是一个没有大小限制的队列,


因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费


者)才会被阻塞。


这里能说出前三种也就差不多了,如果能说全那是最好。


6 ,线程安全需要保证几个基本特征?


原子性 ,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。


可见性 ,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将


线程本地状态反映到主内存上, volatile 就是负责保证可见性的。


有序性 ,是保证线程内串行语义,避免指令重排等。


7 ,说一下线程之间是如何通信的?


线程之间的通信有两种方式:共享内存和消息传递。


共享内存


在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写 - 读内存中的公共状态来


隐式进行通信。典型的共享内存通信方式,就是通过共享对象进行通信。


例如上图线程 A 与 线程 B 之间如果要通信的话,那么就必须经历下面两个步骤:


1. 线程 A 把本地内存 A 更新过得共享变量刷新到主内存中去。


2. 线程 B 到主内存中去读取线程 A 之前更新过的共享变量。


消息传递


在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行


通信。在 Java 中典型的消息传递方式,就是 wait() 和 notify() ,或者 BlockingQueue 。


8 CAS 的原理呢?


CAS 叫做 CompareAndSwap ,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含


三个操作数:


1. 变量内存地址, V 表示


2. 旧的预期值, A 表示


3. 准备设置的新值, B 表示


当执行 CAS 指令时,只有当 V 等于 A 时,才会用 B 去更新 V 的值,否则就不会执行更新操作。


9 CAS 有什么缺点吗?


阿里内部资料 CAS 的缺点主要有 3 点:


ABA 问题 : ABA 的问题指的是在 CAS 更新的过程中,当读取到的值是 A ,然后准备赋值的时候仍然是


A ,但是实际上有可能 A 的值被改成了 B ,然后又被改回了 A ,这个 CAS 更新的漏洞就叫做 ABA 。只是


ABA 的问题大部分场景下都不影响并发的最终效果。


Java 中有 AtomicStampedReference 来解决这个问题,他加入了预期标志和更新后标志两个字段,


更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。


循环时间长开销大 :自旋 CAS 的方式如果长时间不成功,会给 CPU 带来很大的开销。


只能保证一个共享变量的原子操作 :只对一个共享变量操作可以保证原子性,但是多个则不行,多


个可以通过 AtomicReference 来处理或者使用锁 synchronized 实现。


10 、引用类型有哪些?有什么区别?


引用类型主要分为强软弱虚四种:


1. 强引用指的就是代码中普遍存在的赋值方式,比如 A a = new A() 这种。强引用关联的对象,永


远不会被 GC 回收。


2. 软引用可以用 SoftReference 来描述,指的是那些有用但是不是必须要的对象。系统在发生内存


溢出前会对这类引用的对象进行回收。


3. 弱引用可以用 WeakReference 来描述,他的强度比软引用更低一点,弱引用的对象下一次 GC


的时候一定会被回收,而不管内存是否足够。


4. 虚引用也被称作幻影引用,是最弱的引用关系,可以用 PhantomReference 来描述,他必须和


ReferenceQueue 一起使用,同样的当发生 GC 的时候,虚引用也会被回收。可以用虚引用来管


理堆外内存。


11 、说说 ThreadLocal 原理?


hreadLocal 可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部


副本变量就行了,做到了线程之间互相隔离,相比于 synchronized 的做法是用空间来换时间。


ThreadLocal 有一个静态内部类 ThreadLocalMap , ThreadLocalMap 又包含了一个 Entry 数组,


Entry 本身是一个弱引用,他的 key 是指向 ThreadLocal 的弱引用, Entry 具备了保存 key value 键值对


的能力。


弱引用的目的是为了防止内存泄露,如果是强引用那么 ThreadLocal 对象除非线程结束否则始终无


法被回收,弱引用则会在下一次 GC 的时候被回收。


但是这样还是会存在内存泄露的问题,假如 key 和 ThreadLocal 对象被回收之后, entry 中就存在 key


为 null ,但是 value 有值的 entry 对象,但是永远没办法被访问到,同样除非线程结束运行。


但是只要 ThreadLocal 使用恰当,在使用完之后调用 remove 方法删除 Entry 对象,实际上是不会出


现这个问题的。


阿里内部资料


12 、线程池原理知道吗?以及核心参数


首先线程池有几个核心的参数概念:


1. 最大线程数 maximumPoolSize


2. 核心线程数 corePoolSize


3. 活跃时间 keepAliveTime


4. 阻塞队列 workQueue


5. 拒绝策略 RejectedExecutionHandler


当提交一个新任务到线程池时,具体的执行流程如下:


1. 当我们提交任务,线程池会根据 corePoolSize 大小创建若干任务数量线程执行任务


2. 当任务的数量超过 corePoolSize 数量,后续的任务将会进入阻塞队列阻塞排队


3. 当阻塞队列也满了之后,那么将会继续创建 (maximumPoolSize-corePoolSize) 个数量的线程来


执行任务,如果任务处理完成, maximumPoolSize-corePoolSize 额外创建的线程等待


keepAliveTime 之后被自动销毁


4. 如果达到 maximumPoolSize ,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理


阿里内部资料


13 、 线程池的拒绝策略有哪些?


主要有 4 种拒绝策略:


1. AbortPolicy :直接丢弃任务,抛出异常,这是默认策略


2. CallerRunsPolicy :只用调用者所在的线程来处理任务


3. DiscardOldestPolicy :丢弃等待队列中最旧的任务,并执行当前任务


4. DiscardPolicy :直接丢弃任务,也不抛出异常