水手辛巴德

V1

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(链表队列)

AQS
AQS
  1. CLH队列是FIFO队列,故新的节点到来的时候,是要插入到当前队列的尾节点之后
  2. CLH队列它是一个链表队列,通过AQS的两个字段head(头节点)和tail(尾节点)来存取,这两个字段是volatile类型初始化的时候都指向了一个空节点

入队操作

当一个线程成功地获取了同步状态,其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个CAS方法,它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联

出队操作

  1. 因为遵循FIFO规则,所以能成功获取到AQS同步状态的必定是首节点
  2. 首节点的线程在释放同步状态时,会唤醒后续节点
  3. 后续节点会在获取AQS同步状态成功的时候将自己设置为首节点
  4. 设置首节点是由获取同步成功的线程来完成的
  5. 由于只能有一个线程可以获取到同步状态,所以设置首节点的方法不需要像入队这样的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,因此虽然自旋锁的起始开销低于悲观锁,但是随着自旋锁的时间增长,开销也是线性增长的!

分类:

后端

标签:

后端

作者介绍

水手辛巴德
V1