JUC

关键字

synchronized的锁优化有哪些、讲一下锁状态和锁升级

  1. 优化 Monitor 这类的重量级锁 (轻量级锁)
    每个线程中的栈帧都会包含一个锁记录对象(Lock Record),
    内部可以通过 CAS 的方式存储锁定对象的 Mark Word(从而不再一开始就使用 Monitor)

  2. 自旋优化
    当升级到重量级锁竞争时,如果发生竞争失败不会立即进入到 EntryList 进行阻塞,
    而是会重试一会儿再阻塞

  3. 优化轻量级锁重入(偏向锁)轻量级锁在没有竞争时,每次重入操作仍需要 CAS,为了避免性能降低,所以引入了偏向锁优化轻量级锁重入,在第一次 CAS 时会将线程的 ID 写入对象的 Mark Word 中,此后线程发现锁定对象中的 Mark Word 存在自己的线程 ID,则不会再次进行 CAS,因为这个对象就归这个线程所有


voliate关键字

原理:

内存屏障,Memory Barrier(Memory Fence)

对 volatile 变量的写指令后会加入写屏障(屏障之前,对贡献变量的修改都是会同步到主存中)

对 volatile 变量的读指令前会加入读屏障(屏障之后,对共享变量的读取都是主存中的新数据)

作用:

  1. 确保可见性
  2. 确保有序性

保持内存可见性。所有线程都能看到共享内存的最新状态。每次读取前必须先从主内存刷新最新的值。每次写入后必须立即同步回主内存当中。

禁止指令重排。提供内存屏障的方式来防止指令被重排,编译器在生成字节码文件时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

主要类

什么情况下用 ReentrantLock 而不用 synchronized

  1. 阻塞时可被中断
  2. 可以设置获取锁的超时时间
  3. 可以设置为公平锁
  4. 支持多个条件变量(condition)

Java的中断怎么实现,为什么 synchronized 不能中断,ReentrantLock 可以中断

  1. synchronized 是在JVM层面上实现的,在字节码中会有 monitorenter、monitorexit 介入,
    会自动释放锁定(代码执行完成或者出现异常)
  2. ReentrantLock 是实现 lock 接口和内部类继承 AQS 实现的,是通过代码实现的

ReentrantLock 怎么实现的(AQS)

通过实现 lock 接口以及结合继承了 AQS 的内部类 Sync 实现的

AQS源码看过吗?能说一下么?

  1. 使用 CLH 队列,实现线程阻塞等待以及被唤醒时锁分配的机制

  2. 独享模式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    2.1 acquire 操作:
    2.1.1. 查看当前是否有线程占用锁,没有则修改 state 和 exclusiveOwnerThread
    为当前线程,否则加入 CLH 队列
    2.1.2. 加入 CLH 队列的第一个线程,需要初始化头节点,并设置头节点的 waitstatus 为 -1,
    next 节点指向当前线程,当前线程的 pre 指向头节点
    2.1.3. 当有新的线程加入 CLH 队列时,会重复将 pre 指向上一个节点,上一个节点
    的 next 节点指向当前线程并设置上一个节点的 waitstatus 为 -1,而自己的 waitstatus
    设置为 0。重复此操作。
    2.1.4. 当加入到 CLH 队列的线程获取到锁时,会修改 state 和持有锁的线程修改为当前
    线程,并且将 head 移至到当前节点,pre 节点也相应会断掉,之前的 head 节点会被 GC 回收
    2.2 release 操作:
    2.2.1. 检查当前线程是否和 exclusiveOwnerThread 是同一个线程
    2.2.2. 修改锁的状态 status ,具体什么是有锁,什么是无锁,由 AQS 的子类定义
    2.2.3. 唤醒队列中头节点的下一个节点的线程
    2.2.3.1. 将自己的 waitStatus 设为 0
    2.2.3.2. 唤醒线程
  3. 共享模式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    3.1 acquire 操作:
    3.1.1. 检查当前的 state,是否去持有锁。根据不同的 AQS 子类实现,来允许是否
    可以多个线程持有锁比如 CountDownLatch 类不允许,而 ReentrantReadWriteLock 类是允许的
    持有锁时只会去修改 state,而不会去修改 exclusiveOwnerThread
    3.1.2. 其他情况与独享模式类似

    3.2 release 操作:
    3.2.1. 修改 state 字段
    3.2.2. 唤醒队列中头节点的下一个节点,如果被唤醒的节点的下一个节点也是 SHARED 模式,
    则一同唤醒
  4. 条件队列

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    4.1 await 操作
    4.1.1. 创建条件节点,如果是作为第一个节点加入,则需要初始化队列,
    即初始化一个 head 节点,再把自己加入进去,作为 head 节点的 nextWaiter;
    否则直接加入,并使上一个节点的 nextWaiter 指向自己
    4.1.2. 释放当前线程占用的锁,执行 AQS 的 release 操作
    4.1.3. 挂起当前线程
    4.1.4. 被唤醒以后,执行 AQS 的 acquire 操作
    4.2 signal 操作:
    4.2.1. 将队列中的 firstWaiter 节点转移到同步队列中
    4.2.2. 把刚刚加入到同步队列中的节点的前驱的 waitStatus 设置为 -1

ThreadLocal,底层如何实现

每个线程内都有一个ThreadLocalMap类型的成员变量,用来存储资源对象

ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,
那么但凡有一个线程对这个变量进行 set 操作时,这个线程中的 threadLocals 属性就会被创建赋值

所以当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题

1
2
3
4
5
6
7
8
9
public void set(T value) {            
Thread t = Thread.currentThread(); // 拿到当前线程
ThreadLocalMap map = getMap(t); // 拿到当前线程的 threadLocals 属性
if (map != null) {
map.set(this, value); // 不为空进行覆盖
} else {
createMap(t, value); // 为空则创建赋值
}
}

工具类

Java并发包下的原子工具类,能说一下么?源码看过吗?

AtomicBoolean

AtomicInteger

AtomicLong

AtomicReference

AtomicIntegerArray

AtomicLongArray

AtomicReferenceArray

都是以CAS方式确保原子性,但是可能会触发ABA问题

概念

为什么使用CAS就能保证并发?

无需阻塞等待,立马执行,立马返回成功或失败

ABA问题及解决办法

  1. AtomicStampedReference 需要我们传入整型变量作为版本号,来判定是否被更改过
    但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,就使用:
  2. AtomicMarkableReference 需要我们传入布尔变量作为标记,来判断是否被更改过

IO密集和CPU密集两种情况下,线程池里的线程数应该怎么设置

IO密集型的话,是指系统大部分时间在跟I/O交互,而这个时间线程不会占用CPU来处理,即在这个时间范围内,可以由其他线程来使用CPU,因而可以多配置一些线程。

CPU密集型的话,一般配置CPU处理器个数+1个线程, n + 1
所谓CPU密集型就是指系统大部分时间是在做程序正常的计算任务,例如数字运算、赋值、分配内存、内存拷贝、循环、查找、排序等,这些处理都需要CPU来完成。

Java中创建多线程的方式有哪些

  1. 继承Thread,重写run方法;方便传参,但不支持多继承
  2. 使用Runnable配合Thread;解耦强,灵活
  3. FutureTask结合Thread;可以获得放回结果

乐观锁和悲观锁的区别

悲观锁:拿到资源时,就对资源上锁,并在提交后,才释放锁资源,其他线程才能使用资源。

乐观锁:拿到资源时,上乐观锁,在提交之前,其他的锁也可以操作这个资源,当有冲突的时候,并发机制会保留前一个提交,打回后一个提交,让后一个线程重新获取资源后,再操作,然后提交。类似于 Git

吞吐量:乐观锁 > 悲观锁

适用场景:读取频繁用乐观锁,写入频繁用悲观锁

特别地,如果吞吐量大,但是乐观锁获取锁的所消耗的性能又高,这个时候就不推荐适用乐观锁了

什么是死锁,怎么破坏死锁

死锁成立的四个条件:

互斥:某种资源只允许一个进程访问

占有且等待:一个进程本身占有了资源(一个或多个),同时还有资源未得到满足,正在等待其他进程释放该资源

不可抢占:无法抢占别人占有的资源

循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源

打破上述任何一个条件,便可让死锁消失

线程池

线程池有哪些参数

  1. 核心线程数
  2. 最大线程数
  3. 保持存活时间
  4. 时间单位
  5. 线程工厂
  6. 阻塞队列
  7. 拒绝策略

执行流程是什么?

当一个任务传给线程池以后,可能有以下几种可能

  1. 将任务分配给一个核心线程来执行
  2. 核心线程都在执行任务,将任务放到阻塞队列workQueue中等待被执行
  3. 阻塞队列满了,使用救急线程来执行任务;救急线程用完以后,超过生存时间(keepAliveTime)后会被释放
  4. 任务总数大于了 最大线程数(maximumPoolSize)与阻塞队列容量的最大值(workQueue.capacity),使用拒接策略

拒绝策略有哪些?

  1. AbortPolicy(堕胎政策-默认):直接抛出RejectedExecutionException异常阻止系统正常运行
  2. CallerRunsPolicy (调用者运行政策):一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量
  3. DiscardOldestPolicy(丢弃老的政策):抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
  4. DiscardPolicy(丢弃政策):该策略默默地丢弃无法处理的任务,无予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略

JUC
https://huajframe.github.io/2020/11/06/Java并发编程/JUC/
作者
HuaJFrame
发布于
2020年11月6日
许可协议