JUC主要包含
- Lock框架
- Tools类
- Collections: 并发集合
- Atomic: 原子类
- Executors: 线程池
JUC基础: CAS, Unsafe(了解)
JUC中多数类是通过volatile和CAS来实现的,CAS本质上提供的是一种无锁方案,而Synchronized和Lock是互斥锁方案; java原子类本质上使用的是CAS,而CAS底层是通过Unsafe类实现的。
CAS
CAS的全称为Compare-And-Swap,直译就是对比交换。
简单解释:CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
CAS操作是原子性的,所以多线程并发使用CAS更新数据时,可以不使用锁。JDK中大量使用了CAS来更新数据而防止加锁(synchronized 重量级锁)来保持原子更新。
CAS 方式为乐观锁,synchronized 为悲观锁。因此使用 CAS 解决并发问题通常情况下性能更优。
使用CAS方式的几个问题:
ABA问题
因为CAS需要在操作值的时候,检查值有没有发生变化,比如没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时则会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A。
从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行命令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率。
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i = 2,j = a,合并一下ij = 2a,然后用CAS来操作ij。
从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
unsafe
Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。
Unsafe类为一单例实现,提供静态方法getUnsafe获取Unsafe实例,当且仅当调用getUnsafe方法的类为引导类加载器所加载时才合法,否则抛出SecurityException异常。Android 中无法直接使用,只能通过反射的方式获取使用。
Java原子类是通过UnSafe类实现的。
UnSafe类总体功能:
Unsafe的一些基本方法,参考:blog.csdn.net/qq_22685435…
Lock常用类
LockSupport
LockSupport用来创建锁和其他同步类的基本线程阻塞基础。当调用LockSupport.park时,表示当前线程将会等待,直至获得许可,当调用LockSupport.unpark时,必须把等待获得许可的线程作为参数进行传递,好让此线程继续运行。
public class LockSupport {
private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
private static final long PARKBLOCKER;
private static final long SECONDARY;
static {
try {
PARKBLOCKER = U.objectFieldOffset
(Thread.class.getDeclaredField("parkBlocker"));
SECONDARY = U.objectFieldOffset
(Thread.class.getDeclaredField("threadLocalRandomSecondarySeed"));
} catch (ReflectiveOperationException e) {
throw new Error(e);
}
}
private LockSupport() {} // Cannot be instantiated.
private static void setBlocker(Thread t, Object arg) {
U.putObject(t, PARKBLOCKER, arg);
}
//阻塞线程
public static void park() {
U.park(false, 0L);
}
//阻塞线程,设置blocker,可用通过getBlocker,获取设置的值
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
U.park(false, 0L);
setBlocker(t, null);
}
//在许可可用前禁用当前线程,并最多等待指定的等待时间
public static void parkNanos(long nanos) {
if (nanos > 0)
U.park(false, nanos);
}
public static void parkNanos(Object blocker, long nanos) {
if (nanos > 0) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
U.park(false, nanos);
setBlocker(t, null);
}
}
//在指定的时限前禁用当前线程,除非许可可用
public static void parkUntil(long deadline) {
U.park(true, deadline);
}
public static void parkUntil(Object blocker, long deadline) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
U.park(true, deadline);
setBlocker(t, null);
}
public static Object getBlocker(Thread t) {
if (t == null)
throw new NullPointerException();
return U.getObjectVolatile(t, PARKBLOCKER);
}
//如果给定线程的许可尚不可用,则使其可用。如果线程在 park 上受阻塞,则它将解除
//其阻塞状态。否则,保证下一次调用 park 不会受阻塞。如果给定线程尚未启动,则无法保证
//此操作有任何效果。
public static void unpark(Thread thread) {
if (thread != null)
U.unpark(thread);
}
}
-
park函数,阻塞线程,并且该线程在下列情况发生之前都会被阻塞: ① 调用unpark函数,释放该线程的许可。② 该线程被中断。③ 设置的时间到了。并且,当time为绝对时间时,isAbsolute为true,否则,isAbsolute为false。当time为0时,表示无限等待,直到unpark发生。
-
unpark函数,释放线程的许可,即激活调用park后阻塞的线程。这个函数不是安全的,调用这个函数时要确保线程依旧存活。
通过上面的源码发现LockSupport核心函数都是基于Unsafe类中定义的park和unpark函数。
一些总结
Thread.sleep()和Object.wait()的区别
- Thread.sleep()不会释放占有的锁,Object.wait()会释放占有的锁;
- Thread.sleep()必须传入时间,Object.wait()可传可不传,不传表示一直阻塞下去;
- Thread.sleep()到时间了会自动唤醒,然后继续执行;
- Object.wait()不带时间的,需要另一个线程使用Object.notify()唤醒;
- Object.wait()带时间的,假如没有被notify,到时间了会自动唤醒,这时又分好两种情况,一是立即获取到了锁,线程自然会继续执行;二是没有立即获取锁,线程进入同步队列等待获取锁;
他们俩最大的区别就是Thread.sleep()不会释放锁资源,Object.wait()会释放锁资源。
Object.wait()和Condition.await()的区别
Object.wait()和Condition.await()的原理是基本一致的,不同的是Condition.await()底层是调用LockSupport.park()来实现阻塞当前线程的。
实际上,它在阻塞当前线程之前还干了两件事,一是把当前线程添加到条件队列中,二是“完全”释放锁,也就是让state状态变量变为0,然后才是调用LockSupport.park()阻塞当前线程。
Thread.sleep()和LockSupport.park()的区别
LockSupport.park()、parkNanos()、parkUtil()等,我们这里说的park()方法统称这一类方法。
- 从功能上来说,Thread.sleep()和LockSupport.park()方法类似,都是阻塞当前线程的执行,且都不会释放当前线程占有的锁资源;
- Thread.sleep()没法从外部唤醒,只能自己醒过来;
- LockSupport.park()方法可以被另一个线程调用LockSupport.unpark()方法唤醒;
- Thread.sleep()方法声明上抛出了InterruptedException中断异常,所以调用者需要捕获这个异常或者再抛出;
- LockSupport.park()方法不需要捕获中断异常;
- Thread.sleep()本身就是一个native方法;
- LockSupport.park()底层是调用的Unsafe的native方法;
Object.wait()和LockSupport.park()的区别
- Object.wait()方法需要在synchronized块中执行;
- LockSupport.park()可以在任意地方执行;
- Object.wait()方法声明抛出了中断异常,调用者需要捕获或者再抛出;
- LockSupport.park()不需要捕获中断异常;
- Object.wait()不带超时的,需要另一个线程执行notify()来唤醒,但不一定继续执行后续内容;
- LockSupport.park()不带超时的,需要另一个线程执行unpark()来唤醒,一定会继续执行后续内容;
park()/unpark()底层的原理是“二元信号量”,你可以把它相像成只有一个许可证的Semaphore,只不过这个信号量在重复执行unpark()的时候也不会再增加许可证,最多只有一个许可证。
在wait()之前执行了notify()会怎样?
如果当前的线程不是此对象锁的所有者,却调用该对象的notify()或wait()方法时抛出IllegalMonitorStateException异常; 如果当前线程是此对象锁的所有者,wait()将一直阻塞,因为后续将没有其它notify()唤醒它。
在park()之前执行了unpark()会怎样?
线程不会被阻塞,直接跳过park(),继续执行后续内容
LockSupport.park()会释放锁资源吗?
不会,它只负责阻塞当前线程,释放锁资源实际上是在Condition的await()方法中实现的。
ReentrantLock
使用和源码查看:JAVA基础-volatile 、final 、synchronized和ReentrantLock
ReentrantReadWriteLock
ReentrantReadWriteLock是读写锁接口ReadWriteLock的实现类。只要没有writer,读取锁可以由多个reader线程同时保持,这样可以更快的读取数据,一般多用于数据不常变化,read多的情况。
ReentrantReadWriteLock底层是基于ReentrantLock和AbstractQueuedSynchronizer来实现的,ReentrantReadWriteLock的数据结构也依托于AQS的数据结构。它包括Lock子类ReadLock和WriteLock。ReadLock是共享锁,WriteLock是独占锁。
ReentrantReadWriteLock基本使用
public class ReentrantReadWriteLockTest {
private ReentrantReadWriteLock mReentrantReadWriteLock = new ReentrantReadWriteLock();
private int textNum = 0;
public static void main(String[] args) throws InterruptedException {
ReentrantReadWriteLockTest reentrantReadWriteLockTest = new ReentrantReadWriteLockTest();
for (int i = 0; i < 3; i++) {
new Thread(() -> reentrantReadWriteLockTest.read()).start();
}
for (int i = 0; i < 3; i++) {
new Thread(() -> reentrantReadWriteLockTest.write()).start();
}
Thread.sleep(10000);
}
private void read() {
mReentrantReadWriteLock.readLock().lock();
try {
System.out.println("read textNum:" + textNum);
} finally {
mReentrantReadWriteLock.readLock().unlock();
}
}
private void write() {
mReentrantReadWriteLock.writeLock().lock();
try {
textNum++;
System.out.println("write textNum:" + textNum);
} finally {
mReentrantReadWriteLock.writeLock().unlock();
}
}
}
StampedLock
StampedLock是java8在java.util.concurrent.locks新增的一个API。 StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。
对比 ReentrantReadWriteLock 主要不同是该锁不允许重入,多了乐观读的功能,使用上会更加复杂一些,但是具有更好的性能表现。
所谓的乐观读模式,也就是若读的操作很多,写的操作很少的情况下,你可以乐观地认为,写入与读取同时发生几率很少,因此不悲观地使用完全的读取锁定,程序可以查看读取资料之后,是否遭到写入执行的变更,再采取后续的措施(重新读取变更信息,或者抛出异常) ,这一个改进,可大幅度提高程序的吞吐量。
StampedLock 简单例子
public class StampedLockTest {
private StampedLock mStampedLock = new StampedLock();
private int count=0;
//普通读
public void read() {
long l = mStampedLock.readLock();
try{
System.out.println(count);
}finally {
mStampedLock.unlockRead(l);
}
}
//乐观读
public void optimisticRead() {
long stamp = mStampedLock.tryOptimisticRead();//尝试获取乐观读锁
if(!mStampedLock.validate(stamp)){//尝试获取乐观读锁失败
long l = mStampedLock.readLock();//已经进入写模式,没办法只能老老实实的获取读锁
try{
System.out.println(count);
}finally {
mStampedLock.unlockRead(l);
}
}
}
//写
public void write() {
long l = mStampedLock.writeLock();
try{
count++;
}finally {
mStampedLock.unlockWrite(l);
}
}
}
所有方法
Tools常用类(了解)
CountDownLatch
CountDownLatch可以使一个或多个线程等待其他线程各自执行完毕后再执行。
CountDownLatch 定义了一个计数器,和一个阻塞队列, 当计数器的值递减为0之前,阻塞队列里面的线程处于挂起状态,当计数器递减到0时会唤醒阻塞队列所有线程,这里的计数器是一个标志,可以表示一个任务一个线程,也可以表示一个倒计时器,CountDownLatch可以解决那些一个或者多个线程在执行之前必须依赖于某些必要的前提业务先执行的场景。
常用方法说明
- CountDownLatch(int count):构造方法,创建一个值为count 的计数器。
- await():阻塞当前线程,将当前线程加入阻塞队列。
- await(long timeout, TimeUnit unit):在timeout的时间之内阻塞当前线程,时间一过则当前线程可以执行,
- countDown():对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。
简单例子
public class CountDownLatchTest {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
countDownLatch.countDown();
System.out.println(Thread.currentThread().getName()+" game Over");
}).start();
}
try {
countDownLatch.wait();//使用wait会阻塞到countDownLatch 计数器为0
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" game Over");
}
}
CyclicBarrier
CyclicBarrier循环栅栏和CountDownLatch很类似,都能阻塞—组线程。 当有大量线程相互配合,分别计算不同任务,并且需要最后统一汇总的时候,我们可以使用CyclicBarrier。CyclicBarrier可以构造一个集结点,当某一个线程执行完毕,它就会到集结点等待,直到所有线程都到了集结点,那么该栅栏就被撤销,所有线程再统一出发,继续执行剩下的任务。
CyclicBarrier 和 CountDownLatch的区别
作用不同: CyclicBarrier要等固定数量的线程都到达了栅栏位置才能继续执行,而CountDownLatch只需等待数字到0,也就是说 CountDownLatch用于事件,但是CyclicBarrier是用于线程的。 可重用性不同: CountDownLatch在倒数到0并触发门门打开后,就不能再次使用了,除非新建新的实例;而CyclicBarrier可以重复使用。
简单例子
public class CyclicBarrierTest {
private static CyclicBarrier mCyclicBarrier = new CyclicBarrier(3);
public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
for (int i = 0; i < 2; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " going to await");
try {
mCyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + " continue");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
System.out.println(Thread.currentThread().getName() + " going to await");
mCyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + " continue");
}
}
输出:
Thread-2 going to await
main going to await
Thread-3 going to await
Thread-3 continue
Thread-2 continue
main continue
Phaser
Java 7的并发包中推出了Phaser,其功能跟CyclicBarrier和CountDownLatch有些重叠,但是提供了更灵活的用法,例如支持动态调整注册任务的数量等。本文在Phaser自带的示例代码基础上进行一下简单的分析。
注册(Registration)
Phaser支持通过register()和bulkRegister(int parties)方法来动态调整注册任务的数量,此外也支持通过其构造函数进行指定初始数量。在适当的时机,Phaser支持减少注册任务的数量,例如 arriveAndDeregister()。单个Phaser实例允许的注册任务数的上限是65535。
到达(Arrival)
正如Phaser类的名字所暗示,每个Phaser实例都会维护一个phase number,初始值为0。每当所有注册的任务都到达Phaser时,phase number累加,并在超过Integer.MAX_VALUE后清零。arrive()和arriveAndDeregister()方法用于记录到 达,arriveAndAwaitAdvance()方法用于记录到达,并且等待其它未到达的任务。
终止(Termination)
Phaser支持终止。Phaser终止之后,调用register()和bulkRegister(int parties)方法没有任何效果,arriveAndAwaitAdvance()方法也会立即返回。触发终止的时机是在protected boolean onAdvance(int phase, int registeredParties)方法返回时,如果该方法返回true,那么Phaser会被终止。默认实现是在注册任务数为0时返回true(即 return registeredParties == 0;)。此外,forceTermination()方法用于强制终止,isTerminated()方法用于判断是否已经终止。
层次结构(Tiering)
Phaser支持层次结构,即通过构造函数Phaser(Phaser parent)和Phaser(Phaser parent, int parties)构造一个树形结构。这有助于减轻因在单个的Phaser上注册过多的任务而导致的竞争,从而提升吞吐量,代价是增加单个操作的开销。
常用方法
简单例子
public class PhaserTest {
//3表示创建时设置的任务数,可以通过register()和bulkRegister(int parties)方法添加。
private Phaser mPhaser=new Phaser(3){
//方法返回true,触发Phaser终止
//phase 完成的第几个阶段
//registeredParties 完成阶段的任务数
@Override
protected boolean onAdvance(int phase, int registeredParties) {
System.out.println("到达的phase:"+phase+" 完成任务数"+registeredParties);
return super.onAdvance(phase, registeredParties);
}
};
private void task1(){
System.out.println("开始任务1");
mPhaser.arriveAndAwaitAdvance();
System.out.println("完成任务1");
}
private void task2(){
System.out.println("开始任务2");
mPhaser.arriveAndAwaitAdvance();
System.out.println("完成任务2");
}
private void task3(){
System.out.println("开始任务3");
mPhaser.arriveAndAwaitAdvance();
System.out.println("完成任务3");
}
public static void main(String[] args){
PhaserTest phaserTest = new PhaserTest();
System.out.println("开始main任务");
//添加一个新任务
phaserTest.mPhaser.register();
new Thread(()->{
phaserTest.task1();
}).start();
new Thread(()->{
phaserTest.task2();
}).start();
new Thread(()->{
phaserTest.task3();
}).start();
phaserTest.mPhaser.arriveAndAwaitAdvance();
System.out.println("完成main任务");
}
}
输出:
开始main任务
开始任务1
开始任务2
开始任务3
到达的phase:0 完成任务数4
完成任务3
完成main任务
完成任务2
完成任务1
Semaphore
是一种计数器,用来保护一个或者多个共享资源的访问。如果线程要访问一个资源就必须先获得信号量。如果信号量内部计数器大于0,信号量减1,然后允许共享这个资源;否则,如果信号量的计数器等于0,信号量将会把线程置入休眠直至计数器大于0.当信号量使用完时,必须释放。
常用方法
Exchanger
Exchanger用于进行两个线程之间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange()方法交换数据,当一个线程先执行exchange()方法后,它会一直等待第二个线程也执行exchange()方法,当这两个线程到达同步点时,这两个线程就可以交换数据了
常用方法
简单例子
public class ExchangerTest {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger();
new Thread(() -> {
try {
String thread1 = exchanger.exchange("Thread1");
System.out.println(Thread.currentThread().getName() + " " + thread1);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"Thread1").start();
new Thread(() -> {
try {
String thread2 = exchanger.exchange("Thread2");
System.out.println(Thread.currentThread().getName() + " " + thread2);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"Thread2").start();
}
}
输出:
Thread2 Thread1
Thread1 Thread2
并发集合
ConcurrentHashMap
ConcurrentHashMap在JDK1.5~1.7之前使用分段锁机制实现,JDK1.8则使用数组+链表+红黑树数据结构和CAS原子操作实现。
ConcurrentHashMap是线程安全的HashMap,使用同HashMap。
注意:
ConcurrentHashMap的key和value都不能是null。
对比总结
HashTable: 使用了synchronized关键字对put等操作进行加锁;ConcurrentHashMap JDK1.7: 使用分段锁机制实现;ConcurrentHashMap JDK1.8: 则使用数组+链表+红黑树数据结构和CAS原子操作实现;
CopyOnWriteArrayList
CopyOnWriteArrayList是ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的拷贝来实现的
COWIterator类
COWIterator表示迭代器,其也有一个Object类型的数组作为CopyOnWriteArrayList数组的快照,这种快照风格的迭代器方法在创建迭代器时使用了对当时数组状态的引用。此数组在迭代器的生存期内不会更改,因此不可能发生冲突,并且迭代器保证不会抛出 ConcurrentModificationException。创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。在迭代器上进行的元素更改操作(remove、set 和 add)不受支持。这些方法将抛出 UnsupportedOperationException。
CopyOnWriteArrayList 缺点:
- 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc
- 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;
ConcurrentLinkedQueue
ConcurerntLinkedQueue一个基于链接节点的无界线程安全队列。此队列按照 FIFO(先进先出)原则对元素进行排序。队列的头部是队列中时间最长的元素。队列的尾部 是队列中时间最短的元素。新的元素插入到队列的尾部,队列获取操作从队列头部获得元素。
常用方法
BlockingQueue
BlockingQueue接口类 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。
一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的临界点。也就是说,它是有限的。如果该阻塞队列到达了其临界点,负责生产的线程将会在往里边插入新对象时发生阻塞。它会一直处于阻塞之中,直到负责消费的线程从队列中拿走一个对象。 负责消费的线程将会一直从该阻塞队列中拿出对象。如果消费线程尝试去从一个空的队列中提取对象的话,这个消费线程将会处于阻塞之中,直到一个生产线程把一个对象丢进队列。
BlockingQueue一些常用实现类ArrayBlockingQueue,DelayQueue,LinkedBlockingQueue,PriorityBlockingQueue,SynchronousQueue,以及BlockingDeque接口(实现类LinkedBlockingDeque)。
BlockingQueue接口方法:
ArrayBlockingQueue(数组阻塞队列)
ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。你可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改了(译者注: 因为它是基于数组实现的,也就具有数组的特性: 一旦初始化,大小就无法修改)。 ArrayBlockingQueue 内部以 FIFO(先进先出)的顺序对元素进行存储。队列中的头元素在所有元素之中是放入时间最久的那个,而尾元素则是最短的。
DelayQueue(延迟队列)
DelayQueue 对元素进行持有直到一个特定的延迟到期。加入其中的元素必须实现Delayed接口。
当生产者元素调用put往其中加入元素时,使用Delayed接口的compareTo方法进行排序,这个排序是按照时间的,按照计划执行的时间排序,先执行的在前面,后执行的排后面;消费者获取(take)元素时,内部调用getDelay方法返回的值大于0,则消费者线程wait返回的这个时间后(阻塞当前线程),再从队列头部取出元素。
LinkedBlockingQueue(链阻塞队列)
LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。
LinkedBlockingQueue 内部以 FIFO(先进先出)的顺序对元素进行存储。队列中的头元素在所有元素之中是放入时间最久的那个,而尾元素则是最短的那个。 以下是 LinkedBlockingQueue 的初始化和使用示例代码:
PriorityBlockingQueue(优先级的阻塞队列)
PriorityBlockingQueue 是一个无界的并发队列。它使用了和类 java.util.PriorityQueue 一样的排序规则。你无法向这个队列中插入 null 值。 所有插入到 PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排序就取决于你自己的 Comparable 实现。 注意 PriorityBlockingQueue 对于具有相等优先级(compare() == 0)的元素并不强制任何特定行为。
同时注意,如果你从一个 PriorityBlockingQueue 获得一个 Iterator 的话,该 Iterator 并不能保证它对元素的遍历是以优先级为序的。
SynchronousQueue(同步队列)
SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。
LinkedBlockingDeque(链阻塞双端队列)
LinkedBlockingDeque 类实现了 BlockingDeque 接口。双端队列是一个你可以从任意一端插入或者抽取元素的队列。
LinkedBlockingDeque 是一个双端队列,在它为空的时候,一个试图从中抽取数据的线程将会阻塞,无论该线程是试图从哪一端抽取数据
原子类
Java中的原子类是java.util.concurrent.atomic包下的对象,他们之所以有原子性的共性,都来源于CAS。对于原子类变量的操作是不会存在并发性问题的,不需要使用同步手段进行并发控制。它底层自身的实现即可保证变量的可见性以及操作的原子性。
原子类基础类型
- AtomicBoolean 原子更新布尔类型
- AtomicInteger 原子更新整型
- AtomicLong 原子更新长整型
AtomicInteger常用方法(以上3个类提供的方法几乎一模一样):
原子类数组类型
- AtomicIntegerArray 原子更新整型数组里的元素
- AtomicLongArray 原子更新长整型数组里的元素
- BooleanArray 原子更新Boolean数组里的元素
- AtomicReferenceArray 原子更新引用类型数组里的元素
这三个类的最常用的方法是如下两个方法:
- get(int index):获取索引为index的元素值。
- compareAndSet(int i,E expect,E update): 如果当前值等于预期值,则以原子方式将数组位置i的元素设置为update值。
引用
- AtomicReference 原子更新引用类型
- AtomicStampedReference 原子更新引用类型, 内部使用Pair来存储元素值及其版本号。维护了一个版本号,解决ABA问题
- AtomicMarkedReference 原子更新带有标记位的引用类型。也解决了ABA问题。
这三个类提供的方法都差不多,首先构造一个引用对象,然后把引用对象set进Atomic类,然后调用compareAndSet等一些方法去进行原子操作,原理都是基于Unsafe实现,但AtomicReferenceFieldUpdater略有不同,更新的字段必须用volatile修饰。
FieldUpdater
- AtomicLongFieldUpdater 原子更新长整型字段的更新器
- AtomicIntegerFieldUpdater 原子更新整型的字段的更新器
- AtomicReferenceFieldUpdater
使用方式都差不多,是基于反射的原子更新字段的值。要想原子地更新字段类需要两步:
- 第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
- 第二步,更新类的字段必须使用public volatile修饰。
对于AtomicIntegerFieldUpdater 的使用稍微有一些限制和约束,约束如下:
- 字段必须是volatile类型的,在线程之间共享变量时保证立即可见。
- 字段的描述类型(修饰符public/protected/default/private)是与调用者与操作对象字段的关系一致。也就是说调用者能够直接操作对象字段,那么就可以反射进行原子操作。但是对于父类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段。
- 只能是实例变量,不能是类变量,也就是说不能加static关键字。
- 只能是可修改变量,不能使final变量,因为final的语义就是不可修改。实际上final的语义和volatile是有冲突的,这两个关键字不能同时存在。
- 对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型的字段,不能修改其包装类型(Integer/Long)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater。