
水手辛巴德
2022/10/02阅读:38主题:橙心
程序员八股文之Java篇
微信公众号:辛巴德笔记
关注我,一起修炼编程内功,一起加油!
创建线程方式(四种)
-
继承Thread类 -
实现Runable接口 -
实现Callable和Future。可以得到返回结果,并处理异常(CompleteFuture) -
线程池创建
ThreadLocal
运用场景
工具类: 时间类格式化
全局变量: 避免参数传递,在拦截器中获取用户信息、共享session、cookie等
使用要点
-
内存泄漏: -
由于 ThreadLocalMap 的⽣命周期Thread ⼀样⻓
,如果没有⼿动删除对应的key就会导致内存泄露,可以⼿动remove -
Entry中key的弱引用 -
Entry中的value强引用,无法及时回收!导致OOM
-
-
NPE异常: 直接get不会产生NPE,出现原因在于拆装箱导致 -
共享对象: 不应在ThreadLocal中存放静态对象,否则线程不安全 -
优先使用框架
总结
-
线程安全 -
不需要加锁,提高执行效率 -
高效利用内存、节省开销 -
避免传参的繁琐 -
低耦合、更优雅
Synchonized
底层原理
同步方法的实现
-
JVM底层实现,依靠方法修饰符上的 ACC_SYNCHRONIZED
实现
同步代码块的实现
同步代码块的synchronized是使用monitorenter和monitorexit指令来实现的
-
monitorenter指令在编译为字节码后插入到同步代码块的开始位置 -
monitorexit指令在编译为字节码后插入到方法结束位置和异常位置 -
JVM要保证每个monitorenter必须有对应的monitorexit
锁优化过程
无锁
-
不锁住资源,多个线程只有一个线程可修改成功资源,其他线程会重试
偏向锁
-
同一个线程执行同步资源时,自动获取资源 -
只有一个线程会进入临界区
轻量级锁
-
多个线程竞争同步资源时,没有获取资源的线程 自旋等待释放锁
-
多个线程交替进入临界区
重量级锁
-
多个线程竞争同步资源时,没有获取资源的线程 阻塞等待唤醒
-
多个线程同时进入临界区
AQS(Abstract Queue Synchronized)
简介: 如果共享资源被占用了,就需要一定的阻塞等待唤醒机制来保证锁分配,这个机制主要通过CLH队列的实体实现的,将暂时获取不到的锁线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的节点(Node),通过CAS自旋及LockSupport.unpark()、维护State变量的状态,使并发达到同步的控制效果。
作用: 加锁会导致阻塞,有阻塞就需要排队,实现排队必然需要某种形式的队列来进行管理
产生背景
-
CountDownLatch -
购物车拼团、人满发车;1等多,多等1 -
await() -
countDown()
-
-
ReentrantLock -
Semaphore -
acquire():获取到许可证,该线程才可往下进行 -
release():当业务执行完毕后,通过该方法释放许可证,让其他线程获取
-
-
ReentrantReadWriteLock -
CyclicBarrier(可重用) -
类似CountDownLatch -
十几人开会,只有等到10个人到齐才能开始下面流程
-
三大要素
volatile int state
-
CAS指令实现compareAndSetState保证原子性 -
volatile int state 保证可见性、有序性
FIFO队列
因为争抢锁的线程可能很多,但是只能有⼀个线程拿到锁,其他的线程都必须等 待,这个时候就需要⼀个 queue 来管理这些线程,AQS ⽤的是⼀个 FIFO 的队列,就是⼀个链表,每个 node 都持有后继节点的引⽤。
线程的阻塞与释放
例如:acquire()和release()以及park()和unPark()等方式
主要方法及作用
boolean compareAndSetState(int expect, int update);通过CAS设置当前状态,保证状态设置的原子性。
boolean tryAcquire(int arg);钩子方法,独占锁获取同步状态,AQS未具体实现,实现此方法需查询当前同步状态并判断状态是否符合预期,然后再CAS设置同步状态
boolean tryRelease(int arg);钩子方法,同tryAcquire方法,独占锁释放同步状态
int tryAcquireShared(int arg);共享式获取同步状态,返回>0表示获取成功,否则失败
boolean tryReleaseShared(int arg);钩子方法,共享式释放同步状态
boolean isHeldExclusively();当前同步器是否在独占模式下呗线程占用。一般表示该方法是否被当前线程独占
void acquire(int arg);模板方法,独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则会进入同步队列等待
,此方法会调用子类重写的tryAcquire方法
void acquireInterruptibly(int arg);模板方法,与acquire相同,但是此方法可以响应中断
,当前线程未获取到同步状态而进入同步队列中,如果当前线程被中断,此方法会抛出InterruptedException并返回
CLH(链表队列)

-
CLH队列是FIFO队列
,故新的节点到来的时候,是要插入到当前队列的尾节点之后 -
CLH队列它是一个链表队列,通过AQS的两个字段head(头节点)和tail(尾节点)来存取, 这两个字段是volatile类型
,初始化的时候都指向了一个空节点
入队操作
当一个线程成功地获取了同步状态,其他线程将无法获取到同步状态,转而被构造成为节点
并加入到同步队列中,而这个加入队列的过程必须要保证线程安全
,因此同步器提供了一个CAS方法
,它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联
出队操作
-
因为遵循FIFO规则,所以 能成功获取到AQS同步状态的必定是首节点
, -
首节点的线程在释放同步状态时,会唤醒后续节点 -
后续节点会在获取AQS同步状态成功的时候将自己设置为首节点
-
设置首节点是由获取同步成功的线程来完成的 -
由于只能有一个线程可以获取到同步状态,所以 设置首节点的方法不需要像入队这样的CAS操作
AQS应用
AQS被大量的应用在了同步工具上
-
ReentrantLock类使用AQS同步状态来保存锁重复持有的次数 -
ReentrantReadWriteLock类使用AQS同步状态中的16位来保存写锁持有的次数,剩下的16位用来保存读锁的持有次数 -
Semaphore类(信号量)使用AQS同步状态来保存信号量的当前计数 -
CountDownLatch类使用AQS同步状态来表示计数 -
FutureTask类使用AQS同步状态来表示某个异步计算任务的运行状态(初始化、运行中、被取消和完成) -
SynchronousQueues类使用了内部的等待节点,这些节点可以用于协调生产者和消费者
ConCurrentHashMap
如何保证线程安全
JDK1.7
-
分段锁:Segment+ReentrantLock -
计算size()时,采用类似乐观锁,先不加锁统计,若超过3次则对每个Segment加锁后再统计
JDK1.8
-
采用CAS策略,哈希冲突时,对链表或红黑树加synchronized(Synchronized+CAS) -
计算size()时,会维护一个baseCount属性来记录节点数量,每次进行put操作之后,都会CAS自增baseCount
JDK1.7 | JDK1.8 | |
---|---|---|
数据结构 | Segment分段锁,其中Segment继承ReentrantLock | 数组+链表+红黑树 |
线程安全机制 | 分段锁(Segment) | CAS+Synchronized(链表/红黑树) |
锁粒度 | 对需要进行的数据操作Segment加锁 | 对每个数组元素加锁(Node) |
链表转红黑树 | 当链表节点数量>8且总=容量>=64时 | |
查询时间复杂度 | O(n) | O(log(N)) |
CopyOnWriteArrayList
背景: 替代低性能的Collections.synchronizedList()
场景: 都多写少。例如:黑名单、每日更新等
读写规则: 除了写写互斥,其他操作都可同时进行
总结: 只能保证最终一致性;双份对象内存占用
自旋锁的适用场景?
-
一般用于多核服务器,在并发度不是特别高的情况下,比阻塞锁效率高 -
适用于临界区比较短小的情况,否则临界区很大,线程一旦拿到锁,很久以后才会释放,不合适! -
若锁被占用时间很长,那么自旋的线程只会白白浪费CPU资源。在自旋过程中,会一直消耗CPU,因此虽然自旋锁的起始开销低于悲观锁,但是随着自旋锁的时间增长,开销也是线性增长的!
作者介绍
