JUC提供了几个并发工具,比如CountDownLatch,CycelicBarrier,Semaphore和Exchanger。
Semaphore
又名计数信号量,量初始并维护一定数量的许可证,使用之前先要先获得一个许可,用完之后再释放一个许可。当然也可以逆向使用,在构造Semaphore的时候传入0个许可,执行某个操作后添加一个许可,然后再放行等待的线程。
信号量通常用于限制线程的数量来控制访问某些资源,从而达到单机限流的目的。
Semaphore 也是基于AQS框架来实现的,Semaphore 也有公平和非公平之说,Semaphore 支持重入获得许可。
CountDownLatch
允许一个或多个线程等待其它线程完成操作。在初始化的时候给定 CountDownLatch 一个计数,调用await() 方法的线程会一直等待,其他线程执行完操作后调用countDown(),当计数减到0 ,调用await() 方法的线程被唤醒继续执行。
CountDownLatch没有增加计数的API,所以不可以重复使用,如果要用可以重置计数的,可以使用CyclicBarrier。
CountDownLatch可以用在多个线程同时去操作各自的逻辑,等都结束后再统一汇总等业务场景。
Exchanger
用于进行线程间的数据交换。它提供了一个同步点,在这个同步点,两个线程可以进行数据交换,主要是通过exchange()这个方法进行,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange()方法,当两个线程都达到同步点时,就可以将本线程生产出的数据传递给对方。
Exchanger里面有个内部类Node,使用了sun.misc.Contended注解进行修饰,由于最大的缓存行为128个字节,使用sun.misc.Contended来增加padding(要生效还需要虚拟机参数-XX:-RestrictContended的支持),可以使得任意两个可用Node不会再同一个缓存行中。
除了Exchanger直接使用Unsafe里的park与unpark进行线程等待外,其它三个类都是借助AQS完成的。
Exchanger源码可参考:源码分析:Exchanger之数据交换器
JUC
JUC包下可以分为下面几类:
- atomic,基本类型、引用类型、累加器
- locks,读写锁、重入锁
- collections并发容器(或叫非阻塞队列)ConcurrentHashMap/ConcurrentSkipListMap及阻塞队列BlockingQueue/CopyOnWriteArrayList
- executor执行框架与线程池,Future/Executor
tools并发工具类,CountdownLatch/CyclicBarrier/Semaphore/Exchanger
非阻塞队列 ConcurrentLinkedQueue 使用 CAS 非阻塞算法 + 不停重试,来实现线程安全,适合用在不需要阻塞功能;
阻塞队列BlockLinkedQueue主要使用Lock与Condition进行等待唤醒。
AQS
1,state属性
AQS里有个重要的字段state,在不同的类里有着不同的含义:
- 基于state实现的排它锁ReentrantLock,state 值为1代表锁被占用,值为0时代表锁未被占用。重入锁state则为重入的次数。
- 基于state实现的读写锁ReentrantReadWriteLock,state 被分成两部分,高16位记录读锁次数,低16位记录写锁次数。读锁中不同线程获取的读锁次数使用threadlocal保存。
- 基于state实现的信号量Semaphore,初始化一个state值,表示最大限制数,即可以做到允许最多N个线程同时运行,达到限流效果。
- 基于state实现的线程等待器CountDownLatch,初始化一个state值,state值为0时触发唤醒动作。
2,两个队列
AQS里有两个重要的队列:同步队列与等待队列。
- 同步队列:维护唤醒线程的队列,获取互斥锁失败时入队的线程。
- 等待队列:实现条件锁时用到的队列。调用await()的时候会释放锁,然后线程会加入到条件队列,调用signal()唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁。
3,节点状态
AQS 定义了5个队列中节点状态:
- 值为0,初始化状态,表示当前节点在sync队列中,等待着获取锁。
- CANCELLED,值为1,表示当前的线程被取消。
- SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark。
- CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列。
- PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行。
再记录下几个api:
- Thread的Sleep()方法:必须要指定时间;需要捕获异常;不会释放锁
- Object的wait()方法和notify()方法:必须配合sychronized关键字使用;notify()方法在wait()方法之后执行,否则会丢失唤醒信号;需要捕获异常;会释放锁
- Condition接口的await()与Signal()方法:必须配合lock.lock()方法使用:主要是可以定点唤醒。会释放锁。
- LockSupport的park()和unpark(thread)方法:不需要捕获异常;不会释放锁;可以通过unpark唤醒,unpark可以比park先执行,不会丢失唤醒信号。
针对park再提一下,park会阻塞当前线程,下面4种情况会返回:
- 对应的unpark已执行,可前可后
- 线程被中断
- 等待time时间到
- 异常发生时
最后对锁的使用有个整个的说明,如果精细化考虑了锁应用范围后,性能还无法满足需求的话,我们就要考虑另一个维度的粒度问题了,即:区分读写场景以及资源的访问冲突,考虑使用悲观方式的锁还是乐观方式的锁。(参考:《Java业务开发常见错误100例》)
一般业务当中:
- 使用锁时,锁的范围应当尽量小。
- 对于读写比例差异明显的场景,考虑使用 ReentrantReadWriteLock 细化区分读写锁,来提高性能。
- 如果你的 JDK 版本高于 1.8、共享资源的冲突概率也没那么大的话,考虑使用 StampedLock 的乐观读的特性,进一步提高性能。
- JDK 里 ReentrantLock 和 ReentrantReadWriteLock 都提供了公平锁的版本,在没有明确需求的情况下不要轻易开启公平锁特性,在任务很轻的情况下开启公平锁可能会让性能下降上百倍。
参考的文章:《Java并发编程的艺术》
AbstractQueuedSynchronizer(AQS) 总结篇
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。