Java并发编程

多线程初探:线程产生、同步、原理

提交到线程池的可以是:

  • 实现了  Runnable  接口的任务
  • 实现了  Callable  接口的任务
  • FutureTask  类型的任务
    而 FutureTask 既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。用于异步计算?因为可以之后再 get() 线程的结果.

object.wait() 必须在 synchronized 方法中使用,会自动放弃锁,直到别的线程 notify 才能进入 RUNNABLE 状态,等待线程调度。
object.sleep() 并不会放弃锁

线程组的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ThreadGroup implements Thread.UncaughtExceptionHandler {
private final ThreadGroup parent; // 父亲ThreadGroup
String name; // ThreadGroup 的名称
int maxPriority; // 最大优先级
boolean destroyed; // 是否被销毁
boolean daemon; // 是否守护线程
boolean vmAllowSuspension; // 是否可以中断

int nUnstartedThreads = 0; // 还未启动的线程
int nthreads; // ThreadGroup中线程数目
Thread threads[]; // ThreadGroup中的线程

int ngroups; // 线程组数目
ThreadGroup groups[]; // 线程组数组
}
  • 一个进程创建的所有线程,都是共享一个内存空间的;cpu 切换任务时机是在指令级,不在语言级别。同时还需要考虑编译器优化,指令重排对多线程访问有序性的影响。
  • 多线程编程出现并发错误的源头在于:可见性问题(cpu 缓存是的不同线程读取到了未更新的数据)、有序性问题(编译器优化使得在不同时间片切换,线程访问错误)
  • Happens-Before 规则:前面一个操作的结果对后续操作是可见的(这里指的都是对于共享变量的操作,是可见的)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 有趣的双重检查创建单例代码
public class Singleton {
static volatile Singleton instance; // 加上volatile 禁止指令重排序才能创建合理的单例

static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}

😭 指令重排(有序性问题)可能会导致顺序 分配内存(memory = allocate())、将 instance 指向这块内存(instance = memory)、 初始化对象(调用构造方法)。这导致其他线程可能访问了未初始化完全的变量。Java 使用了 volatile:禁止指令重排序 解决此问题。所以这个例子目前是可行的。

volatile 关键字怎么解决并发中的经典问题
禁止线程用自己的缓存,使得线程对变量的修改可以让所有的线程可见!和禁止指令重排,解决有序性问题

🤔JVM 会等待工作线程全部结束之后才会停止主线程服务?
是的,JVM 确实会等待,但并不意味着主线程 main 也会等待,这就是我们需要 join 的原因。join 会让主线程阻塞从而等待子线程优先于主线程结束,执行逻辑正确;否则主线程可能中途结束执行到了后面的逻辑。

同步原语 synchronized:

  • static synchronized function()会在类级别的上加锁,整个 JVM 只有一把锁。
  • 🤔 如何锁住其他对象呢?传递 object 对象作为参数,锁住 object。这也意味每次要锁住都必须传入相同的 object,会有难度

线程池:

  • 关注 corePoolSize,线程池保证这部分线程始终能够接受任务,不会轻易回收。而 corePoolSize <= 线程数量<= MaxiumPoolSize 这部分非核心线程会在任务完成之后一段时间内被回收。
  • 如果 MaxiumPoolSize 设置非常大,或者 workQueue 是无界队列,拒绝策略将不会生效,永远堆不满。
  • ❌ 缓存(无线制队列和任务)、简单(只能一个)、定长线程池(core=maxium)都太极端,自定义的可以。

并发同步:锁、线程本地

⭐Lock 接口:
✅ 可重入锁:其本身含义是指 同一个线程可以多次获取已经持有的锁
ReentrantLock 有 lock() 、trylock() 方法,其中 trylock() 有返回值 boolean,true 时获得锁进入,立即返回。等待时间内获取到锁,trylock( long time )是一段时间的自旋锁。❓ 是不是可以缓解死锁的出现呢
注意,finally 语句中都要将锁释放,lock.unlock( )。
公平性上,默认的都是非公平锁,意味着线程不一定按照到达锁的时间按顺序排队获得锁,而是按照策略选择一个队列中的线程执行。

✅ 可重入读写锁:读锁是共享锁,多个线程可同时进入临界区;写锁是独占锁。
他们在公平性上,对于同一份资源,如果读锁在队列的头部,那么新的读线程可能会插队;反之,如果是写锁,那么读线程会排到队列的末尾。提高性能之举。

❓ 关于 reentrantlock 和 synchronized 关键字的对比

对比维度 synchronized ReentrantLock
所在包 java 关键字,属于 JVM 语法级别 java.util.concurrent.locks 中的类
加锁/释放方式 自动加锁、自动释放(异常也会自动释放) 需要手动加锁 lock() 和释放 unlock(),要写在 finally
可重入性 ✅ 是 ✅ 是
公平锁支持 ❌ 不支持,默认非公平 ✅ 可设置为公平锁(先来先得)
可中断锁 ❌ 不支持 ✅ 支持 lockInterruptibly()
尝试获取锁(不阻塞线程) ❌ 不支持 ✅ 支持 tryLock()
是否支持条件变量 ❌ 不支持(只能用 wait()/notify() ✅ 支持 Condition 类,可实现多个等待队列
性能(JDK1.6 之后) 已优化,引入偏向锁/轻量级锁,性能很好 高并发下性能优秀,功能灵活
可见性与原子性 ✅ 内置保证(由 JVM 实现) ✅ 依赖底层实现,效果相同
锁的粒度 隐式对象级别 灵活控制(可锁方法块、不同对象)
使用难度 ✅ 简单,关键字一写就好 ⚠️ 稍复杂,需配合 try-finally 写法(finally 里面写上 unlock,防止并发错误)
reentrantlock 更适合可中断、限时等待、多条件变量的场景,当然 synchronized 关键字已经优化的很好了

并发流程控制

多个线程之间存在流程控制怎么办,考虑并发顺序和流程,而非并发安全问题!

特性 CountDownLatch CyclicBarrier Semaphore
等待机制 等待 N 次 countDown() 等待 N 个线程 await() 获取 N 个许可才可进入
可否重用 ❌ 不可重用 ✅ 可重用 ✅ 可重用
回调功能(触发动作) ❌ 没有 ✅ 有(barrier action) ❌ 没有
应用场景 等待其他线程完成(等所有学生到了再开班会,只关心人数是否齐全) 线程“集结点”,到达屏障点之后再统一执行任务。可以一环扣着一环 控制并发访问,允许最多多少个线程同时进入临界区
控制的是谁 主线程等待子线程 所有线程互相等待 控制访问资源的线程数量
实现原理 AQS 共享锁 AQS + 屏障机制 AQS 共享锁 + 许可证信号量
用法 await( ) 和 countdown( )配合, await 用来等待 await( )阻塞标记单个任务执行完毕,计数器-1 直到为 0 执行回调函数

✅CountDownLatch 适合协调多个线程完成某项任务(等待一组任务完成后再执行):
使用 await( ) countdown( ) api 用于同步。当 count 减少到 0 之后 await 阻塞。主线程调用 await() 等待其他线程调用 countDown()

✅CyclicBarrier 适合多线程并行执行后统一汇总(后面还有统一的任务):

❌threadlocal??

  1. 线程隔离作用:每个线程都有着自己的资源,避免线程之间的共享导致并发不安全。另外实现类:threadlocal 的 getter、setter、remove 方法
  2. 上下文特性,减少线程内同一个实例反复传递参数繁琐:

    threadlocal 内存泄漏问题?

CAS 原子操作

CAS 基于乐观锁的思想实现,底层是硬件上的 compare and set,解决 “一个变量在多线程下原子修改” 问题。 涉及到 current value、expect value、set value,如果当前值和期望值相同,那么就会设置新的值。这个过程会保证自旋,设置自旋失败次数上限防止长时间占用锁。和悲观锁 synchroized reentrantLock 不同的是,cas 是多线程同时进入的,他们各自拿到内存的值,get compare if correct then set,而悲观锁确保在同一时间只能有一个线程拿到锁(非读锁、信号量)

❓ 是不是自旋一定会带来并发性能下降?不一定,如果临界区很小,线程不是很多,频繁的切换线程浪费更多。

类型 具体类 说明
Atomic* 基本类型原子类 AtomicInteger、AtomicLong、AtomicBoolean 基本类型的原子自增操作
Atomic*Array 数组类型原子类 AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
Atomic*Reference 引用类型原子类 AtomicReference(只能保证引用对象原子更新,但是内部字段不保证)、AtomicStampedReference、AtomicMarkableReference 不替换整个对象的条件下,对字段进行原子更新。字段必须是 volatile,保证内存可见
Atomic*FieldUpdater 升级类型原子类 AtomicIntegerfieldupdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
Adder 累加器 LongAdder、DoubleAdder 使用分段锁,维护 Cell 数组中每个 cell 增加的值最终累加
Accumulator 积累器 LongAccumulator、DoubleAccumulator

分段/分区来减少锁冲突,一种将资源拆成多段,每段各自加锁的并发策略。LongAdder 底层维护 cell 数组(根据当前的线程数确定数组的大小),每个线程只在各自的 cell 上累加,不会出现冲突。也是分治思想

atomic API 接口参考

并发容器


Java并发编程
http://whale-withme.github.io/JAVA并发编程/
作者
yzc
发布于
2025年5月13日
许可协议