同步相关

锁的实现

synchronized关键字

两种实现方式:

  1. 锁住方法

    1. 锁住普通方法

    2. 锁住类方法

  2. 锁住对象

锁住的无非是两个

  1. 当前对象this

  2. 类对象xx.class,该对象是唯一的。

底层原理

重量级锁

查看字节码指令发现是在同步段前后获得了对象的monitormonitor就是管程的实现,synchronized关键字和wait,notiy,notifyAll都是管程的组成部分。

monitor是有ObjectMonitor实现的,它包含一个同步队列,一个等待队列

  1. _owner:初始值为NULL,当有线程占有该monitor时,设置为该线程的ID,当线程释放时,再置为NULL,JVM通过CAS操作保证其线程安全。

  2. _cxq:竞争队列所有请求锁的线程首先会被放入这个队列中。使用头插法维护。

  3. _EntryList:cxq队列中有资格成为候选资源的线程会被移动到该队列中。

  4. _WaitSet:等待队列,因为调用wait方法而被阻塞的线程会被放入该队列中。

monitor竞争过程

  1. 通过CAS尝试把monitor的owner值设置为当前线程id

  2. 若owner已经指向当前线程,则记录重入次数recursions++

  3. 若第一次进去该monitor,设置为recursions = 1owner指向当前线程,该线程成功获得锁并返回;

  4. 若操作失败,则等待锁的释放。

monitor等待

  1. 当前线程被封装成ObjectMonitor对象node;

  2. for循环使用CAS把node节点插入到cxq链表中;

  3. node节点被push到cxq中后,通过自旋尝试获取锁,如果还是没有获得锁,则通过park将当前线程挂起等待被唤醒;

  4. 当线程被唤醒时会从挂起的点继续执行,再尝试获取锁;

notify可以唤醒同一个monitor下调用wait挂起的线程。

锁的状态

在JVM中对象的布局

  1. 对象头

    1. Mark Word:锁信息、GC年龄,hashcode等

    2. Klass Pointer:指向类元数据的指针

  2. 实例数据

  3. 对其填充

偏向锁

通过对比Mark Word中的Thread id来解决加锁问题。只有一个线程执行同步块时提供性能。

流程:当线程访问同步块时获取锁的处理过程

  1. 检查Mark Word中的线程id

  2. 如果为空,则用CAS替换为当前线程id,替换成功->获取锁成功,替换失败->撤销

  3. 如果不为空,则检查是否是当前线程id,若是->获取成功,若不是->撤销

撤销

  1. 撤销动作需要等待全局安全点(当前状态下,堆对象是确定一致的,JVM可以安全地进行操作)

  2. 暂停拥有锁的线程,判断锁对象是否处于被锁定状态

  3. 恢复到无锁或者升级到轻量级锁。

轻量级锁

通过CAS操作Mark Word和自旋来解决解锁问题,避免线程阻塞和唤醒从而影响性能。

场景:两条线程交替执行,不存在竞争。

加锁: 多个线程竞争偏向锁导致锁升级

  1. JVM在当前线程的栈帧中创建锁记录,将对象MarkWord复制到锁记录中。

  2. 线程尝试使用CAS将对象头中的MarkWord和锁记录替换

    1. 替换成功,获得锁

    2. 替换失败,检查对象的mark word是否指向当前线程的栈帧(栈帧中可以存在多个Lock Record,只有第最开始创建的LockRecord中会复制MarkWord,后面创建的LockRecord中Displaced Mark Word=null用来表示重入计数)

      1. 是,获得锁

      2. 否,获得锁失败,膨胀为重量级锁。

释放锁

  1. 使用CAS将MarkWord还原

  2. 若1执行成功,则释放

  3. 若1执行失败,则膨胀为重量级锁

重量级锁

除了拥有锁的线程以外的线程都阻塞。

ReentrantLock

可重入锁,用同步状态State来控制整体可重入情况。

有两个内部类实现了公平锁ReentrantLock.FairSync和非公平锁ReentrantLock.NonfairSync

加锁

  1. lock,暴露给用户的API

  2. acquire,AQS核心方法

  3. tryAcquire->nonfairTryAcquire,自定义同步器实现方法

解锁

  1. unlock,暴露给用户的API

  2. release,AQS核心方法

  3. tryRelease,自定义同步器实现方法

AQS原理

AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。

核心思想:如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。

这个机制就是由CLH队列的变体实现(虚拟双向队列)

数据结构分析

  1. Node节点,用于实现双向等待队列

  2. state,表示同步状态,可以实现独占和共享两种模式

同步状态分析

  1. 执行aquire(1)时,会通过TryAcquire获取锁,在这种情况下,如果获取失败,则调用addWaiter加入到等待队列中

  2. addWaiter就是一个在双端链表添加尾节点的操作,需要注意的是,双端链表的头结点是一个无参构造函数的头结点。

  3. acquireQueued会把放入队列中的线程不断去获取锁,直到获取成功或者不再需要获取(中断)。

synchronized和ReentrantLock的区别

ReentrantLock

synchronized

锁实现机制

依赖AQS

监视器模式

灵活性

支持响应中断、超时、尝试获取锁

不灵活

锁释放形式

显示调用unlock释放

自动释放

锁类型

公平&非公平锁

非公平锁

条件队列

可以关联多个条件队列

关联一个队列

可重入性

性能

适用于大量同步的场景下

适用于少量同步的情况下(竞争不激烈)

ReentrantReadWriteLock

使用AQS同步状态中的16位保存写锁持有的次数,用剩下的16位保存读锁持有的次数。

阻塞队列

四种等级的读写

抛出异常

特殊值

阻塞

阻塞超时

Insert

add

offer

put

offer(e, time)

Remove

remove

poll

take

poll(time)

Examine

element

peek

/

/

生产者消费者问题

volatile关键字

Java内存模型

  1. 主内存

  2. 工作内存,线程私有,保存了该线程使用到的变量的主内存副本拷贝。线程对变量的读写都必须在工作内存中进行,不能直接读写主内存中的变量。

特性

volatile关键字修饰的变量具有两种特性:

  1. 保证此变量对所有线程的可见性:当一条线程修改了这个值,新值对于其他线程来说是可以立即得知的。

  2. 禁止指令重排序优化。

底层原理

可见性

对被volatile修饰的关键字赋值后,多执行了lock addl $0x0, (%esp)操作。这个操作相当于一个内存屏障,将这个缓存中的变量写回主存。由于其他处理器遵守缓存一致性协议,CPU发现自己缓存中对应的内存地址被修改后,就会将CPU的缓存设置为无效状态。当处理器对这个数据进行修改操作的时候,会强制重新从主存把数据读到处理器缓存。

有序性

不能将后面的指令重排序到内存屏蔽之前的位置。

JUC中的工具类

AtomicInteger

i++的区别,i++操作不是原子性操作。

CAS

Compare and swap。三个参数:内存地址,期待值,更新值

  1. 如果内存地址上的值 == 期待值,则将其更新为更新值

  2. 如果不相等,则返回false

应用

  1. 原子类

  2. AQS中修改同步状态

ABA

在更新后追加版本号,版本号每次都累加。

BlockingQueue

CopyOnWriteArray

通过写复制实现读写分离。

线程池

本质是对任务和线程的管理,思想是将其解耦,不让两者关联,

好处:

  1. 降低资源的消耗

  2. 提高响应速度

  3. 线程复用,可以控制最大并发数,方便管理线程

7大参数

  1. corePoolSize

  2. maximumPoolSize

  3. keepAliveTime

  4. time unit

  5. Blocking queue

  6. Thread factory

  7. reject policy

3大方法

  1. Executors.newSingleThreadExecutor() c=1,m=1

  2. Executors.newFixedThreadExecutor(n) c=n, m = n

  3. Executors.newCachedThreadExecutor() c = 0, m = Integer.MAX_VALUE

任务管理模块

任务调度

任务调度是线程池的主要入口,该部分是线程池核心运行机制

  1. 检查线程池状态,如果不是RUNNING,则拒绝;

  2. workerCount < coreSize ,创建新线程去执行该任务;

  3. coreSize < workerCount && !queue.full,将该任务存入任务队列中;

  4. coreSize < workderCount && queue.full && workerCount < maxSize,创建新线程去执行新提交的任务;

  5. wokerCount == maxSize && queue.full,采用拒绝策略处理该任务;

任务缓冲

用阻塞队列来缓存任务。

任务申请

任务的执行存在两种方式

  1. 直接创建线程并执行任务

  2. 线程从任务队列中获取任务然后执行任务

getTask方法实现线程获取任务

  1. 获取线程池状态,若已经停止,则返回null

  2. 获取线程数,若数量过多,则返回null

  3. 判断该线程是否是可回收线程

    1. 是,限时获取任务

    2. 否,阻塞获取任务

任务拒绝

线程池最大任务数(容量)= 最大线程数 + 阻塞队列容量。

当超出容量时,就要采取拒绝策略,保护线程池。

四种拒绝策略:

  1. AbortPolicy 弹出异常

  2. CallerRunsPolicy 让调用者执行该任务

  3. DiscardPolicy 直接丢弃

  4. DiscardOldestPolicy 丢弃最早的未处理的线程

线程管理模块

线程状态

  1. 新建

  2. 就绪

  3. 运行

  4. 阻塞

  5. 终止

实现原理

线程池为了掌握线程的状态并维护线程的生命周期,设计了线程池内的工作线程Worker,它继承了AQS

private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
    final Thread thread;// Worker持有的线程
    Runnable firstTask;// 初始化的任务,可以为null
}

线程池使用HashSet去持有线程的引用,通过添加、移除来控制线程的生命周期。

如何判断线程的状态:继承了AQS来实现独占锁

  1. 若worker获取了独占锁,表示线程正在执行

  2. 若worker处于空闲状态,没有独占锁,则说明它没有处理任务

Worker线程增加

addWorker(firstTask, core)方法

Worker线程回收

线程池中线程的销毁依赖JVM自动的回收,线程池做的工作是根据当前线程池的状态维护一定数量的线程引用,防止这部分线程被JVM回收,当线程池决定哪些线程需要回收时,只需要将其引用消除即可。

Worker被创建出来后,就会不断地进行轮询,然后获取任务去执行,核心线程可以无限等待获取任务,非核心线程要限时获取任务。当Worker无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用。

核心线程默认不会被回收掉,如果设置了allowCoreTimeOut=true时,当核心线程空闲时,会被回收。

线程池在业务中的实践

快速响应用户请求(计算密集型)

场景:购物车案例。获取最大的响应速度去满足用户,所以不应该设置阻塞队列去缓存并发任务,调高core和maxSize去尽可能创建更多的线程快速执行任务。

快速处理批量任务(IO密集型)

场景:离线的大量计算任务。任务量大,不需要瞬时完成。应该设置阻塞队列去缓冲并发任务,调整合适的核心数量。

Last updated

Was this helpful?