JSR 介绍
JSR,全称 Java Specification Requests, 即Java规范提案, 主要是用于向 JCP(Java Community Process) 提出新增标准化技术规范的正式请求。每次 JAVA 版本更新都会有对应的 JSR 更新,比如在 Java 8 版本中,其新特性 Lambda 表达式对应的是 JSR 335,新的日期和时间API对应的是 JSR 310。
JSR 166 是 Doug Lea 提出的一个关于 Java 并发编程的规范提案。JDK1.5 之前,我们控制程序并发访问同步代码只能使用 synchronized,那个时候 synchronized 的性能还没优化好,性能并不好,控制线程也只能使用 Object 的 wait 和 notify 方法。这个时候 Doug Lea 给 JCP 提交了 JSR-166 的提案,在提交 JSR-166 之前,Doug Lea 已经使用了类似 J.U.C 包功能的代码已经三年多了,这些代码就是 J.U.C 的原型。
J.U.C,即 java.util.concurrent
的缩写,该包参考自EDU.oswego.cs.dl.util.concurrent,是JSR 166标准规范的一个实现。
- Doug Lea 主页:Doug Lea’s Home Page
- JSR-166:Concurrency JSR-166 Interest Site
- JSR 166 Slider:JSR-166: Concurrency Utilities
- java.util.concurrent JavaDoc: JDK 1.5 、 JDK 7 、 JDK 8 、 JDK 9
JSR-166 包括多个规范,每个规范都引入了一些新的接口和类,以下是详细描述:
JSR-166(Java SE 5)
:定义了Java并发包的核心接口和类,包括 Executors 框架、Queues、Timing、Synchronizers、Concurrent Collections、Memory Consistency Properties、Atomic、Locks 等。这些接口和类提供了一种方便、高效、可扩展的方式来处理异步任务和并发编程。JSR-166x(Java SE 7)
:定义了Java并发包中的一些新特性,包括 Phaser、TransferQueue、Exchanger、LinkedTransferQueue 等接口和类。其中 Phaser 支持分阶段执行任务,TransferQueue 和 LinkedTransferQueue 实现了高效的生产者-消费者模式,Exchanger 支持两个线程之间交换数据。JSR-166y(Java SE 8)
:定义了Java并发包中的一些新特性,包括 StampedLock、CompletableFuture、LongAdder 等接口和类。其中 StampedLock 是一种乐观锁,支持读写分离,CompletableFuture 支持异步任务执行和结果处理,LongAdder 是一种高效的计数器。JSR-166z(Java SE 9)
:定义了 Java 并发包中的一些新特性,包括 VarHandle、Fences 等接口和类。其中 VarHandle 提供了一种更加灵活的原子操作方式,Fences 提供了一些方法用于控制内存屏障。
JUC
java.util.concurrent 包下的类以及引入版本(没有标注版本号的为 1.5
):
- java.util.concurrent
- java.util.concurrent.locks
- AbstractOwnableSynchronizer
1.6
- AbstractQueuedLongSynchronizer
1.6
- AbstractQueuedSynchronizer
- Condition
- Lock
- LockSupport
- ReadWriteLock
- ReentrantLock
- ReentrantReadWriteLock
- StampedLock
1.8
- AbstractOwnableSynchronizer
- java.util.concurrent.atomic
- AtomicBoolean
- AtomicInteger
- AtomicIntegerArray
- AtomicIntegerFieldUpdater
- AtomicLong
- AtomicLongArray
- AtomicLongFieldUpdater
- AtomicMarkableReference
- AtomicReference
- AtomicReferenceArray
- AtomicReferenceFieldUpdater
- AtomicStampedReference
- DoubleAccumulator
1.8
- DoubleAdder
1.8
- LongAccumulator
1.8
- LongAdder
1.8
- AbstractExecutorService
- ArrayBlockingQueue
- BlockingDeque
1.6
- BlockingQueue
- BrokenBarrierException
- Callable
- CancellationException
- CompletableFuture
1.8
- CompletionException
1.8
- CompletionService
- CompletionStage
1.8
- ConcurrentHashMap
- ConcurrentLinkedDeque
1.7
- ConcurrentLinkedQueue
- ConcurrentMap
- ConcurrentNavigableMap
1.6
- ConcurrentSkipListMap
1.6
- ConcurrentSkipListSet
1.6
- CopyOnWriteArrayList
- CopyOnWriteArraySet
- CountDownLatch
- CountedCompleter
1.8
- CyclicBarrier
- Delayed
- DelayQueue
- Exchanger
- ExecutionException
- Executor
- ExecutorCompletionService
- Executors
- ExecutorService
- Flow
1.9
- ForkJoinPool
1.7
- ForkJoinTask
1.7
- ForkJoinWorkerThread
1.7
- Future
- FutureTask
- LinkedBlockingDeque
1.6
- LinkedBlockingQueue
- LinkedTransferQueue
1.7
- Phaser
1.7
- PriorityBlockingQueue
- RecursiveAction
1.7
- RecursiveTask
1.7
- RejectedExecutionException
- RejectedExecutionHandler
- RunnableFuture
- RunnableScheduledFuture
- ScheduledExecutorService
- ScheduledFuture
- ScheduledThreadPoolExecutor
- Semaphore
- SubmissionPublisher
1.9
- SynchronousQueue
- ThreadFactory
- ThreadLocalRandom
1.7
- ThreadPoolExecutor
- TimeoutException
- TimeUnit
- TransferQueue
1.7
- java.util.concurrent.locks
大致可以分为以下几类:
- 原子更新
- 锁和条件
- 线程池
- 并发容器
- 同步器
在学习 JUC
之前我们需要了解 CAS
,AQS
和 Unsafe
。
- CAS:
- AQS:
- Unsafe:
CAS
CAS(Compare and Swap
)是一种基于原子性操作的并发编程技术,常用于实现线程安全的数据结构和算法。CAS操作由三个参数组成:内存位置V、期望值A、新值B。当且仅当V的值等于A时,CAS操作才会将V的值设置为B,否则不做任何操作。它的实现原理可以简单概括为以下几个步骤:
- 读取内存位置V的值,同时记录下该值的版本号或标记位。
- 检查内存位置V的值是否等于期望值A。如果相等,则执行第3步;否则,操作失败。
- 将新值B写入内存位置V,并更新其版本号或标记位。
- 返回操作结果。
CAS操作是一种乐观锁
机制,它不需要锁定整个共享资源,而是只针对需要修改的值进行原子性操作,从而避免了锁的竞争和开销。在执行CAS操作时,线程会对内存位置进行读取和写入,但同时也会检查内存位置的版本号或标记位,以保证操作的原子性和一致性。
需要注意的是,如果多个线程同时执行CAS操作,可能会出现ABA问题。例如,线程A读取内存位置V的值为A,然后线程B将V的值修改为B,最后线程B又将V的值修改为A。此时,线程A执行CAS操作时,会发现内存位置V的值还是A,虽然这个A的版本号或标记位与之前不同,但线程A并不知道V的值曾经被修改过,因此会将新值写入内存位置V,从而导致数据不一致。为了解决ABA问题,可以使用带有版本号或标记位的CAS操作,或者使用其他的并发编程技术,例如锁或读写锁。
Java中的AtomicXXX
类实现了CAS操作,例如AtomicInteger、AtomicLong等。这些类提供了一组原子性操作方法,例如get()、set()、addAndGet()、compareAndSet()等,它们可以被多个线程安全地使用。
CAS操作虽然免去了锁的开销,但也存在一些问题。首先,CAS操作需要进行多次尝试,直到成功为止。如果并发程度较高,多个线程同时进行CAS操作,可能会导致大量的CAS操作失败,从而降低性能。其次,CAS操作只能保证单个变量的原子性操作,无法保证多个变量之间的操作的原子性,因此需要额外的措施来保证多个变量之间的一致性。
下面是一个使用AtomicInteger实现简单计数器的例子:
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger value = new AtomicInteger(0);
public void increment() {
int oldValue, newValue;
do {
oldValue = value.get();
newValue = oldValue + 1;
} while (!value.compareAndSet(oldValue, newValue));
}
public int getValue() {
return value.get();
}
}
在上面的示例中,increment()
方法使用do-while循环和compareAndSet()
方法执行CAS操作来增加计数器的值。该方法重复使用get()
方法读取计数器的当前值,计算新值,然后尝试使用compareAndSet()
方法更新计数器。循环将继续,直到CAS操作成功并且计数器成功更新。
getValue()
方法使用get()
方法简单地返回计数器的当前值。
需要注意的是,在使用CAS操作时,需要小心处理潜在的ABA问题,其中共享变量的值可能在初始读取和更新尝试之间多次更改。一种处理方法是在共享变量中使用版本号或时间戳,以确保更新仅在值未更改的情况下成功。
ABA问题是在使用CAS(Compare-and-Swap
)操作进行并发编程时经常遇到的一个问题。它发生在一个线程从共享内存位置读取一个值,然后另一个线程将该值更改为另一个值,最后又将其更改回原始值,从而使第一个线程的操作意外成功。
为了处理ABA问题,常用的方法是在共享内存位置中添加一个版本号或时间戳。版本号或时间戳可以在每次修改内存位置时进行递增或更新。这可以确保CAS操作不仅检查值,还检查内存位置的版本号或时间戳。
以下是使用版本号处理ABA问题的示例:
import java.util.concurrent.atomic.AtomicStampedReference;
public class ConcurrentStack<T> {
private AtomicStampedReference<Node<T>> top = new AtomicStampedReference<>(null, 0);
public void push(T value) {
Node<T> newHead = new Node<>(value);
int[] stampHolder = new int[1];
Node<T> oldHead;
do {
oldHead = top.get(stampHolder);
newHead.next = oldHead;
stampHolder[0]++;
} while (!top.compareAndSet(oldHead, newHead, stampHolder[0] - 1, stampHolder[0]));
}
public T pop() {
Node<T> oldHead;
int[] stampHolder = new int[1];
do {
oldHead = top.get(stampHolder);
if (oldHead == null) {
return null;
}
} while (!top.compareAndSet(oldHead, oldHead.next, stampHolder[0], stampHolder[0] + 1));
return oldHead.value;
}
private static class Node<T> {
private final T value;
private Node<T> next;
private Node(T value) {
this.value = value;
}
}
}
在上面的示例中,ConcurrentStack
类使用 AtomicStampedReference
存储栈顶节点。AtomicStampedReference
类存储值的引用和版本号,版本号在引用更改时进行更新。
push()
方法使用新值创建一个新的 Node
,然后尝试使用CAS操作将其推入栈中。循环将继续,直到CAS操作成功,节点成功推入栈中。
pop()
方法尝试使用CAS操作从栈中弹出顶部节点。循环将继续,直到顶部节点成功弹出或栈为空为止。
通过使用具有版本号的 AtomicStampedReference
,ConcurrentStack
类可以处理在并发操作中可能发生的ABA问题。
AQS
AQS(AbstractQueuedSynchronizer
)是Java中用于实现同步器(如锁,信号量等)的框架,它提供了一些基本的同步操作,例如获取锁
、释放锁
、等待条件
、唤醒线程
等。
AQS的实现原理基于一个双向链表,用于维护等待线程的队列。当一个线程需要获取同步器时,它会首先尝试使用CAS操作来获取同步器,如果获取成功,则继续执行;如果获取失败,则将线程加入等待队列中,并将其挂起。当同步器释放时,它会唤醒等待队列中的一个或多个线程,并将它们从等待队列中移除,使它们可以继续执行。
AQS的等待队列是通过一个双向链表来实现的,每个节点代表一个等待线程,节点中包含了线程的状态以及等待条件等信息。等待队列中的节点是按照等待时间的先后顺序排列的,先等待的线程排在前面,后等待的线程排在后面。当一个线程被唤醒时,它会重新尝试获取同步器,如果获取成功,则继续执行;如果获取失败,则它会再次加入等待队列中,并将自己挂起。
AQS的具体实现是通过重写其内部的一些方法来实现的。例如,tryAcquire()
方法用于实现获取同步器的逻辑,它会首先尝试使用CAS操作来获取同步器,如果获取成功,则返回 true;否则返回 false。tryRelease()
方法用于实现释放同步器的逻辑,它会释放同步器,并唤醒等待队列中的一个或多个线程。tryAcquireShared()
和 tryReleaseShared()
方法则用于实现共享式同步器的逻辑,它们类似于 tryAcquire()
和 tryRelease()
方法,但是可以支持多个线程同时获取或释放同步器。
Unsafe
Unsafe类是Java中一个非常特殊且强大的类,它提供了一些不安全的操作,例如直接操作内存、线程挂起和恢复等。Unsafe类是Java中少数几个不被公开支持的类之一,它主要被用于Java核心库和其他一些高级框架中,如Netty、Hadoop和Kafka等。
由于Unsafe类提供了一些不安全的操作,因此它的使用需要非常小心。如果不正确地使用Unsafe类,可能会导致程序崩溃或安全漏洞。因此,Java官方并不建议开发人员使用Unsafe类,而是建议开发人员使用更加安全和标准的Java API。
Unsafe类中一些常用的方法包括:
allocateMemory(long size)
:分配一段指定大小的内存空间。freeMemory(long address)
:释放指定地址的内存空间。putXXX(Object target, long offset, XXX value)
:将指定类型的值写入目标对象的指定偏移量处。getXXX(Object target, long offset)
:从目标对象的指定偏移量处读取指定类型的值。park(boolean isAbsolute, long time)
:挂起当前线程,直到被其他线程唤醒或指定的时间到期。unpark(Thread thread)
:恢复指定线程的运行。
需要注意的是,Unsafe类中的大部分方法都是native方法,实现方式依赖于底层操作系统和硬件平台。这意味着Unsafe类中的方法在不同的平台上可能会有不同的行为,因此需要针对不同的平台进行测试和验证。
Java 9中官方提出了移除Sun.misc.Unsafe类,并在该版本中将该类标记为不推荐使用。然而,由于Unsafe类在Java语言生态中的应用非常广泛,许多框架和库都依赖于Unsafe类来实现高性能和低层次的操作。因此,在Java 9中,官方引入了jdk.internal.misc.Unsafe类来替代Sun.misc.Unsafe类的功能,以保持对Java生态中使用Unsafe类的支持。