1. 什么是JUC
1.1 JUC简介
-
JUC(Java Util Concurrent)是Java中的一个并发工具包,提供了一系列用于多线程编程的类和接口,旨在简化并发编程并提高其效率和可维护性。JUC库包含了许多强大的工具和机制,用于线程管理、同步和协调。
1.2 并发与并行
并发和并行的区别
1. 并发
早期计算机的 CPU 都是单核的,一个 CPU 在同一时间只能执行一个进程/线程,当系统中有多个进程/线程等待执行时,CPU 只能执行完一个再执行下一个。
为了提高 CPU 利用率,减少等待时间,人们提出了一种 CPU 并发工作的理论。
所谓并发,就是通过一种算法将 CPU 资源合理地分配给多个任务,当一个任务执行 I/O 操作时,CPU 可以转而执行其它的任务,等到 I/O 操作完成以后,或者新的任务遇到 I/O 操作时,CPU 再回到原来的任务继续执行。
下图展示了两个任务并发执行的过程:
2. 并行
并发是针对单核 CPU 提出的,而并行则是针对多核 CPU 提出的。和单核 CPU 不同,多核 CPU 真正实现了“同时执行多个任务”。
多核 CPU 的每个核心都可以独立地执行一个任务,而且多个核心之间不会相互干扰。在不同核心上执行的多个任务,是真正地同时运行,这种状态就叫做并行。
例如,同样是执行两个任务,双核 CPU 的工作状态如下图所示:
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你吃一会饭,再去打一会电话,然后再继续吃饭,如果速度足够快,就给人一种吃饭打电话同时进行的感觉,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
3. 并发+并行
在下图中,执行任务的数量恰好等于 CPU 核心的数量,是一种理想状态。但是在实际场景中,处于运行状态的任务是非常多的,尤其是电脑和手机,开机就几十个任务,而 CPU 往往只有 4 核、8 核或者 16 核,远低于任务的数量,这个时候就会同时存在并发和并行两种情况:所有核心都要并行工作,并且每个核心还要并发工作。
例如一个双核 CPU 要执行四个任务,它的工作状态如下图所示:
每个核心并发执行两个任务,两个核心并行的话就能执行四个任务。当然也可以一个核心执行一个任务,另一个核心并发执行三个任务,这跟操作系统的分配方式,以及每个任务的工作状态有关系。
1.3 进程和线程
1.3.1 进程
-
指系统中正在运行的一个应用程序的一个实例
1.3.2 线程
-
一个进程包含多个线程,系统分配处理器时间资源的基本单位,或者说进程内独立执行的一个单元执行流,是程序执行的最小单位
线程的状态
-
Java 中的线程可以处于多种状态,这些状态反映了线程在其生命周期中的不同阶段。以下是 Java 线程可能经历的状态:
-
NEW(新建):
-
线程对象已经创建,但尚未启动。
-
线程还没有调用 start() 方法。
-
RUNNABLE(可运行):
-
线程已经启动,并且准备好运行(正在等待 CPU 时间片)。(就绪)
-
线程可能正在执行(运行中)
-
BLOCKED(阻塞):
-
当线程试图获取一个已经由其他线程持有的锁时,它会被阻塞直到锁被释放。
-
WAITING(等待):
-
线程处于无限期等待状态,等待其他线程执行特定的动作。
-
这种状态通常是由调用
Object.wait()
等方法引起的。
-
TIMED_WAITING(限时等待):
-
线程处于有限期等待状态,等待其他线程执行特定的动作或等待特定的时间间隔。
-
这种状态通常是由调用
Thread.sleep(long millis)
、Object.wait(long timeout)
等方法引起的。
-
TERMINATED [ˈtɜːmɪneɪtɪd](终止):
-
线程已经执行完毕或因异常而终止。
-
线程已经完成了它的任务或被强制停止。
wait和sleep的区别
-
使用方式
-
wait()
方法属于Object
类, -
sleep()
方法属于Thread
类,是一个静态方法,
-
对锁的影响
-
wait()
会释放锁 -
sleep()
不会释放锁,抱着锁睡。
-
如何唤醒
-
wait()
方法需要手动唤醒 notify()或notifyAll() -
sleep()
自己到点了,会自己苏醒
2. java.util.concurrent.locks
2.1 Lock
-
可中断的锁等待:lockInterruptibly()方法允许等待锁的线程响应中断,而不会像synchronized那样一直等待下去。
-
尝试非阻塞的获取锁:tryLock()方法尝试获取锁,如果当前锁不可用,立即返回false,不会阻塞线程。
-
超时获取锁:tryLock(long time, TimeUnit unit)在指定的时间内尝试获取锁,超时后返回false。
2.2 ReentrantLock
2.2.1 主要特点
-
可重入:线程可以多次获取同一个锁,而不会导致死锁。
-
非阻塞锁尝试:提供了
tryLock
方法,可以在没有立即获得锁时选择返回而不是阻塞。 -
锁的公平性和非公平性:可以控制锁的分配策略,决定是否按照请求顺序分配锁。
-
条件变量:提供了
Condition
对象,可以用来实现更复杂的同步行为。
2.2.2 公平性和非公平性
-
构造函数:
ReentrantLock()
创建一个默认为非公平的锁实例。 -
构造函数:
ReentrantLock(boolean fair)
创建一个锁实例,如果fair
为true
则创建一个公平锁,否则创建一个非公平锁。 -
公平锁保证锁的获取顺序遵循先进先出(FIFO)的原则,即线程按照请求锁的顺序来获取锁。这意味着一个线程一旦开始等待锁,它将按照其等待时间的先后顺序获得锁。
-
非公平锁不保证锁的获取顺序,线程在尝试获取锁时可以直接尝试获取,而无需考虑是否有其他线程正在等待。如果获取失败,则线程会进入等待队列。
2.2.3 主要方法
-
lock()
:获取锁。如果锁已被其他线程持有,则当前线程将阻塞,直到锁可用。 -
tryLock()
:尝试获取锁。如果锁可用,则获取锁并立即返回true
;如果锁不可用,则立即返回false
。 -
tryLock(long time, TimeUnit unit)
:尝试获取锁,在指定的时间内等待锁,如果在指定时间内未能获取到锁,则返回false
。 -
unlock()
:释放锁。 -
newCondition()
:创建一个新的Condition
对象,可以用于 更0\复杂的同步操作。
2.3 ReentrantReadWriteLock
主要特点
-
读-写分离:
-
读锁(
ReadLock
): 允许多个线程同时获取读锁,只要没有写锁被持有。 -
写锁(
WriteLock
): 如果某个线程持有了写锁,则不允许其他线程获取任何类型的锁(读锁或写锁)。
-
可重入性:
-
一个已经持有某种锁的线程可以再次获取同一类型的锁而不会发生死锁。
-
例如,如果一个线程已经获取了写锁,它可以再次请求写锁而不被阻塞。
-
公平性和非公平性:
-
ReentrantReadWriteLock
支持公平和非公平两种模式。 -
公平模式下,锁的获取顺序遵循请求顺序。
-
非公平模式下,锁可能优先于队列中的等待线程被授予给新请求。
2.4 4 synchronized
-
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
-
修饰代码块,被修饰的代码块称为同步代码块,作用范Synchronized围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象
public void method() {
synchronized (this) {
// 同步代码块
}
}
修饰方法,被修饰的方法称为同步方法,其作用范围是整个方法,作用对象是调用这个方法的对象
public synchronized void synchronizedMethod() {
// 方法体
}
-
对于同步实例方法和同步代码块,锁是当前对象 (
this
)。对于同步静态方法,锁是当前类的Class
对象。
2.5 Lock和synchronized的区别
-
概念
-
Lock是接口
-
synchronized是关键字
-
使用方式
-
synchronized使用在方法或代码块上,会自己解锁
-
Lock需要自己上锁,自己解锁
-
锁的力度
-
synchronized不能把控
-
Lock可以把控
-
公平性
-
synchronized是非公平锁
-
Lock可以设置它的公平性(默认非公平)
3. java.util.concurrent.atomic
-
java.util.concurrent.atomic
包提供了原子变量类,这些类支持原子操作,即不可中断的操作。这些类主要用于多线程环境中的高效并发编程,可以避免使用锁带来的性能开销。
3.1 原子变量类
-
AtomicInteger
-
主要方法
-
get()
: 获取当前值。 -
set(int newValue)
: 设置当前值。 -
incrementAndGet()
: 原子地将当前值加 1 并返回结果。 -
decrementAndGet()
: 原子地将当前值减 1 并返回结果。 -
addAndGet(int delta)
: 原子地将当前值加上指定值并返回结果。 -
getAndIncrement()
: 原子地将当前值加 1 并返回原来的值。 -
getAndDecrement()
: 原子地将当前值减 1 并返回原来的值。 -
getAndAdd(int delta)
: 原子地将当前值加上指定值并返回原来的值。 -
compareAndSet(int expect, int update)
: 如果当前值等于预期值,则原子地将当前值设置为更新值;否则返回 false。
-
AtomicLong
-
AtomicLong
与AtomicInteger
类似,但处理的是64位的长整型值。
-
AtomicReference
-
AtomicReference
提供了一个引用类型的原子类,它可以存储任何类型的对象。这个类非常有用,因为它可以用来创建复杂的原子数据结构,比如原子队列、栈等。
-
AtomicBoolean
-
AtomicBoolean
是一个布尔型的原子类,提供了线程安全的布尔值更新。
3.2 原子数组
-
AtomicIntegerArray
: 一个整型原子数组。 -
AtomicLongArray
: 一个长整型原子数组。 -
AtomicReferenceArray<T>
: 一个引用类型原子数组。
这些类提供了与原子变量相似的方法,但针对数组元素:
-
get(index)
: 返回指定索引处的值。 -
compareAndSet(index, expectedValue, newValue)
: 如果指定索引处的值等于预期值,则以原子方式将其设置为给定的新值。 -
getAndIncrement(index)
: 以原子方式将指定索引处的值加一并返回旧值。 -
getAndDecrement(index)
: 以原子方式将指定索引处的值减一并返回旧值。 -
getAndAdd(index, delta)
: 以原子方式将指定索引处的值加上给定的增量并返回旧值。 -
addAndGet(index, delta)
: 以原子方式将指定索引处的值加上给定的增量并返回新值
3.3 计数器(LongAdder)
-
LongAdder
是 Java 8 引入的一个高性能的计数器类,它位于java.util.concurrent.atomic
包中。LongAdder
主要用于需要频繁更新和读取大量数据的并发场景,它通过使用无锁技术以及基于分段的计数器来实现高性能的原子操作。 -
方法
-
add(long x)
: 原子性地增加指定的值。 -
increment()
: 原子性地增加1。 -
decrement()
: 原子性地减少1。 -
reset()
: 重置计数器。 -
sum()
: 获取当前的总和
-
LongAdder性能比AtomicLong高
4. 辅助类
4.1 CountDownLatch
-
它允许一个或多个线程等待其他线程完成某些操作。
CountDownLatch
类似于一个倒计时计数器,当计数器的值减至零时,所有等待的线程将被释放继续执行。 -
主要方法
-
构造函数:
CountDownLatch(int count)
创建一个带有指定计数的CountDownLatch
实例。 -
countDown():每次调用此方法都会使计数减一。
-
await():阻塞当前线程,直到计数变为零或超时。
-
await()
:等待计数变为零。 -
await(long timeout, TimeUnit unit)
:等待计数变为零,或者达到指定的超时时间。
4.2 CyclicBarrier
-
CyclicBarrier是Java中的一个同步辅助类,它可以让多个线程互相等待,直到所有线程都到达屏障点后才继续执行。需要传入一个整数作为参与线程的数量,当每个线程调用
await()
方法时,它们都会被阻塞,直到所有线程都调用了await()
方法后才会一起继续执行 -
主要方法
-
构造方法
-
CyclicBarrier(int parties)
: 创建一个新的 CyclicBarrier,参数parties
指的是需要等待的线程数量。 -
CyclicBarrier(int parties, Runnable barrierAction)
: 与第一个构造函数类似,但当所有线程都到达屏障时,会有一个额外的Runnable
任务被执行
-
成员方法
-
await():阻塞当前线程,直到所有参与的线程都调用了此方法。
-
reset(): 重置屏障,取消所有正在等待的线程。
-
getParties(): 返回屏障的参与者数量。
-
isBroken(): 返回一个布尔值表示屏障是否已被中断或超时。
4.3 Semaphore
-
信号量,用于控制同时访问某个资源的线程数量。它的本质是一个“共享锁“。信号量维护了一个信号量许可集。线程可以通过调用 acquire()来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。 线程可以通过release()来释放它所持有的信号量许可
-
主要方法
-
构造函数:
-
Semaphore(int permits)
:创建一个具有给定数量许可的Semaphore
。 -
Semaphore(int permits, boolean fair)
:创建一个具有给定数量许可的Semaphore
,并且可以选择是否公平分配许可。
-
获取许可:
-
acquire()
:等待直到获取一个许可,如果所有许可都被占用,则线程将被阻塞。 -
acquireUninterruptibly()
:等待直到获取一个许可,即使线程被中断也不会抛出异常。 -
tryAcquire()
:尝试获取一个许可,如果许可可用则立即返回true
,否则返回false
。 -
tryAcquire(long timeout, TimeUnit unit)
:尝试在给定时间内获取一个许可,如果在超时时间内许可可用则立即返回true
,否则返回false
。
-
释放许可:
-
release()
:释放一个许可,增加可用的许可数量。
-
查询状态:
-
availablePermits()
:返回当前可用的许可数量。 -
getQueueLength()
:返回等待获取许可的线程数量。 -
hasQueuedThreads()
:返回是否有线程正在等待获取许可。 -
drainPermits()
:获取并消耗所有可用的许可。
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "running...");
sleep(1000);
System.out.println(Thread.currentThread().getName() + "end");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
semaphore.release();
}
}, "线程" + i);
thread.start();
}
}
}
-
示例代码中创建一个许可量为3的信号量,使用for循环创建10个线程去模拟请求信号量限制的资源,运行代码就会发现,某一时刻最多只有3个线程能访问。
4.4 CountDownLatch,CyclicBarrier,Semaphore的区别
-
CountDownLatch、CyclicBarrier、Semaphore都是Java并发库中的同步辅助类,它们都可以用来协调多个线程之间的执行。
-
CountDownLatch是一个计数器,它允许一个或多个线程等待其他线程完成操作。它通常用来实现一个线程等待其他多个线程完成操作之后再继续执行的操作。
-
CyclicBarrier是一个同步屏障,它允许多个线程相互等待,直到到达某个公共屏障点,才能继续执行。它通常用来实现多个线程在同一个屏障处等待,然后再一起继续执行的操作。
-
Semaphore是一个计数信号量,它允许多个线程同时访问共享资源,并通过计数器来控制访问数量。它通常用来实现一个线程需要等待获取一个许可证才能访问共享资源,或者需要释放一个许可证才能完成操作的操作。
-
CountDownLatch适用于一个线程等待多个线程完成操作的情况
-
CyclicBarrier适用于多个线程在同一个屏障处等待
-
Semaphore适用于一个线程需要等待获取许可证才能访问共享
5. ConcurrentHashMap
-
ConcurrentHashMap
是Java中一个线程安全的HashMap
实现,它被设计用于在多线程环境中高效地存储键值对。ConcurrentHashMap
首次出现在Java 5(JDK 1.5)中,并且在Java 8中进行了重大的改进。 -
在Java 5中,
ConcurrentHashMap
使用了分段锁(Segmented Locking)的策略来提高并发性能。Java 8中的ConcurrentHashMap
放弃了分段锁的策略,转而采用了更简单的CAS(Compare and Swap)+ synchronized的组合方式。这样做的目的是降低锁竞争,并简化实现。 -
在ConcurrentHashMap中,key和value都不允许为null
主要方法
-
get(key)
:获取键对应的值。 -
remove(key)
:删除指定键的映射。 -
containsKey(key)
:检查是否存在指定的键。 -
forEach(BiConsumer)
:遍历所有键值对。 -
computeIfAbsent(key, mappingFunction)
:如果键不存在,则计算并放入新值。