1. sychronied 修饰普通方法和静态方法的区别, 什么是可见性
对象锁是用于对象实例方法, 或者一个对象实例上的, 类锁是用于类的静态方法或者一个类的 class
对象上的. 类的对象实例可以有多个, 但是每个类只有一个 class
对象, 所以不同对象实例的对象锁是互不干扰的, 但是每个类只有一个类锁. 但是类锁只是一个概念上的, 并不是真实存在的, 类锁其实是每个类对应的 class
对象. 类锁和对象锁之间是互不干扰的.
可见性是指当多个线程访问同一个变量时, 一个线程修改了这个变量的值, 其他线程能够立即看得到修改的值. 由于变成对变量的所有操作都必须在工作内存中进行, 而不能直接读写主内存中的变量, 那么对于共享变量首先是在自己的工作内存, 之后再同步到主内存. 可是并不会及时刷新到主内存中, 而是会存在一定的时间差, 所以这时候线程 A 对共享变量的操作对于线程 B 来说, 就不具备可见性了. 要解决可见性的问题 , 可以使用 volatile
关键字或者加锁.
2. synchronized 的原理以及与 ReentrantLock 的区别.
synchronized
属于独占式悲观锁, 是通过 JVM 隐式实现的, synchronized
只允许同一时刻只有一个线程操作资源. 它涉及了两条指令 monitorenter / monitorexit
, 每个对象都有一个监视器锁 monitor
, monitor
被占用时就会处于锁定状态, 线程执行 monitorenter
指令时尝试获取 monitor
的所有权, 执行 monitorexit
时释放 monitor
对象. 当其他线程没有拿到 monitor
对象时, 则需要阻塞等待获取该对象.
而对于同步方法是依赖了方法修饰符 flags
上的 ACC_SYNCHRONIZED
实现的, JVM 就是根据该标识符来实现方法同步的. 当方法被调用时, 调用指令将会检查方法的 ACC_SYNCHRONIZED
访问标志是否被设置, 如果设置了, 执行线程将先获取 monitor
, 获取成功之后才能执行方法体. 执行完后再释放 monitor
. 在方法执行期间, 其他任何线程无法再获得同一个 monitor
对象.
ReentrantLock
是 Lock
的默认实现方式之一, 它是基于 AQS 实现的. 通过内部的一个 state
字段来表示锁是否被占用. 0 表示未占用, 此时线程就可以通过 CAS 操作将 state
改为 1成功获取锁. 而其他线程只能排队等待获取资源.
两者相同点:
- 都是用来协调多线程对共享对象, 变量的访问
- 都是可重入锁, 同一线程可以多次获得同一把锁
- 都保证了可见性和互斥性.
两者不同点
-
ReentrantLock
显示的获得,释放锁,synchronized
隐式的获得, 释放锁. -
ReentrantLock
可相应中断,synchronized
无法相应中断. -
ReentrantLock
同时实现了公平锁. -
ReentrantLock
可以知道有没有成功获取锁. -
ReentrantLock
在发生异常时, 如果没有在finally
中主动通过unlock()
释放锁, 则可能造成死锁线程. 而synchronized
发生异常时, 会主动释放锁.
ReentrantLock
的可以看 从 LockSupport 到 AQS 的简单学习中有源码分析
3. synchronized 做了哪些优化
JDK 1.6 引入了自旋锁, 适应性自旋锁, 锁消除, 锁粗化, 以及锁的升级等技术来减少锁操作的开销.
- 锁消除, 会通过逃逸分析的方式, 去分析加锁的代码是否被一个或者多个线程使用, 或者等待被使用. 如果分析证实, 只有一个线程访问, 在编译这个代码段的时候, 就不会生成
synchronized
关键字, 仅仅生代码对应的机器码 - 适应性自旋锁: 简单来说, 就是线程如果自旋成功了, 则下次自旋的次数会更多, 如果自旋失败了, 则自旋的次数减少.
- 锁粗化: 将临近的代码块用同一个锁合并起来. 消除无意义的锁获取和释放, 可以提高程序运行性能
- 锁升级: 无锁状态 -> 偏向锁状态 -> 轻量级锁状态 -> 重量级锁.
4. volatile 原理
通过使用 Lock
前缀的指令将当前处理器缓存行的数据写回到主内存, 将其他处理器的缓存无效. 需要数据操作的时候需要再次去主内存中读取.
通过插入内存屏障指令来禁止会影响变量可见性的指令重排序.指令如下
- 在每个
volatile
写操作的前面插入一个StoreStore
屏障. - 在每个
volatile
写操作的后面插入一个StoreLoad
屏障. - 在每个
volatile
读操作的后面插入一个LoadLoad
屏障. - 在每个
volatile
读操作的后面插入一个LoadStore
屏障.
详情见:从 java 内存模型到 volatile 的简单理解
5. volatile 和 synchronize 有什么区别?
volatile
是最轻量的同步机制, 它保证了不同线程对这个变量进行操作时的可见性, 即一个线程修改了某个变量的值, 这个值对其他线程来说是立即可见的. 但是无法保证操作的原子性, 因此多线程下写的复合操作会导致线程安全问题.
synchronize
可以修饰方法或者以同步块的形式来进行使用, 确保了多个线程在同一时刻只能有一个线程处于方法或者同步块中, 保证了线程对变量访问的可见性和排他性, 又称为内置锁机制.
6. CAS 无锁编程的原理
线程处理器基本都支持 CAS 指令, 只不过实现的算法不同, 每一个 CAS 操作都分为三个运算符, 一个内存地址 V, 一个期望值 A 和一个新值 B. 操作的时候如果这个地址上存放的值等于期望值 A, 则将地址上的值更新为 B. 否则不做任何操作.
CAS 的基本思路就是: 如果这个地址上的值和期望值相等, 则给其赋予新值, 否则不做任何事. 但是要返回原值是多少. 循环 CAS 就是在一个循环里不断的做 CAS 操作, 直到成功为止.
使用 CAS 同时带来了三大问题
ABA 问题: 简单来说就是说一个原值是 A , 有一个线程将其变成了 B 接着又变成了 A, 那么使用 CAS 进行比较检查的时候发现值是没有任何变化的. 但是实际上也是发生了变化. 解决方法是使用版本号的方式来解决. , 就是在变量前追加上版本号. 每次变量更新的时候把版本号加 1, 那么 A-B-A, 就变成了 1A-2B-3A. JDK 也同样提供了两个类来帮助我们实现这个版本号的问题, 分别是
AtomicStampedReference
,AtomicMarkableReference
.循环时间久开销大: 自旋 CAS 如果长时间都不成功, 那么会给 CPU 带来非常大的执行开销. 如果 JVM 能支持处理器提供的
pause
指令, 那么效率会有一定的提升.pause
指令有两个作用, 第一可以延迟流水线执行指令, 使 CPU 不会消耗过多的执行资源. 第二可以避免在退出循环的时候因内存顺序冲突而引起的 CPU 流水线被清空. 从而提高 CPU 执行效率.只能保证一个共享变量的原值操作: 当对一个共享变量进行操作时, 可以怂恿自旋 CAS 的方式来保证原子性, 但是对于多个共享变量操作时, 自旋 CAS 就无法保证其原子性, 这时候就可以使用锁机制, 也可使用 JDK 提供的
AtomicReference
原子操作类来保证引用对象之间的原子性, 就可以将多个原子变量放到一个对象里进行 CAS 操作.
详情见:java 基础回顾 - 基于 CAS 实现原子操作的基本理解
7. AQS 原理
AQS 即抽象的队列同步器. 是用来构建锁或者其他同步组件的基础框架. 不能被实例化, 设计之初就是为了让子类通过继承 AQS 并实现它的抽象方法来管理同步状态. 如 ReentrantLock, ReentrantReadWriteLock, CountDownLatch
就是基于 AQS 实现的.
AQS 是基于 CLH 队列的变体实现的, 是一个双向同步队列. 它获取不到共享资源的线程封装成为一个 Node
节点加入到队列中, 每个 Node
节点维护一个 prev
与 next
引用. 分别指向自己的前驱节点与后置节点. 通过 CAS, 自旋, 以及 LockSuppor.park()
等方式维护内部的一个使用 volatile
修饰的 int
类型共享变量 state
的状态, 使并发达到同步的效果.
详情见: 从 LockSupport 到 AQS 的简单学习
8. 线程的声明周期
Java 中线程的状态分为 6 种.
- 初始状态: 新建了一个线程对象, 但是还未调用
start
方法. - 运行状态: Java 线程中奖就绪
(ready)
和运行(running)
两种状态笼统的成为 运行状态.- 线程对象创建后, 其他线程
(比如 main 线程)
调用了该对象的start()
方法, 该状态的线程位于可运行线程池中, 等待被线程调度选中, 从而获得 CPU 的使用权, 此时就处于就绪状态(ready)
. - 就绪状态的线程在获得CPU 时间片后变为运行中状态
(running)
.
- 线程对象创建后, 其他线程
- 阻塞状态.
- 等待状态: 进入该状态的线程需要等待其他线程做出一些特定动作
(通知或中断)
- 超时等待: 该状态不同与等待状态, 它可以在指定的时间后执行返回.
- 终止状态: 表示该线程已经执行完毕.
详情见: java 基础回顾 - 线程基础
9. sleep, wait, yield 的区别, wait 的线程如何唤醒
sleep, yield
被调用后, 都不会释放当前线程所持有的锁.
调用 wait
方法会释放当前线程持有的锁, 而且被唤醒后会重新去竞争锁, 锁竞争到后才会执行 wait
方法后面的代码.
wait
通常被通用语线程间交互.
sleep
通常被用于暂停执行.
yield
方法使当前线程让出 CPU 执行权.
调用wait
方法后使用 notify / notifyAll
进行唤醒.
9. ThreadLocal 是什么
ThreadLocal
为每个线程都提供了变量的副本, 是的每个线程在某一时间访问到的并非同一对象, 这样就隔离了多个线程对数据的数据共享. 在内部实现上, 每个线程内部都有一个 ThreadLocalMap
, 用来保存每个线程所拥有的变量副本.
详情可见: Android 消息机制之 ThreadLocal 深入源码分析 [ 二 ]
10. 为什么要使用线程池
合理的使用线程池能够带来下面三个好处.
- 降低资源消耗. 通过重复利用已创建的线程降低线程创建和销毁造成的消耗.
- 可以控制最大并发数. 避免大量线程之间因相互抢占系统资源而导致的阻塞现象.
- 提高线程的可管理性. 线程是稀缺资源, 如果无限制的创建, 不仅会消耗系统资源, 还会降低系统的稳定性. 使用线程池可以进行统一分配, 调优和监控.
线程池执行任务的流程
- 如果当前运行的线程小于
corePoolSize
, 则创建新线程来执行任务. - 如果运行的线程等于或大于
corePoolSize
, 则将新任务加入到BlockingQueue
. - 如果无法加入
BlockingQueue
(队列已满), 则创建新的线程来处理任务. - 如果创建新线程使当前运行的线程超出了
maximumPoolSize
, 那么执行拒绝策略.
线程池中的几种拒绝策略为一下几种
-
AbortPolicy
: 直接抛出异常. 线程池中默认的拒绝策略. -
DiscardOldestPolicy
: 直接丢弃阻塞队列中最老的任务, 也就是最前面的任务, 并执行当前任务. -
CallerRunsPolicy
: 提交任务所在的线程来执行这个要提交的任务. -
DiscardPolicy
: 直接丢弃最新的任务, 也就是最后面的.
也可以根据场景来实现RejectedExecutionHandler
接口, 自定义拒绝策略, 比如记录日志或者持久化存储被拒绝的任务.
详情见: 重识 java 线程池