【语言学习】JAVA源码学习 - hippowc/hippowc.github.io GitHub Wiki
java.util.concurrent
概览
按代码功能来看,分为五部分:
- locks部分:显式锁(互斥锁和读写锁);
- atomic部分:原子变量类相关,是构建非阻塞算法的基础;
- executor部分:并发执行框架、线程池相关;
- collections部分:并发容器相关;
- tools部分:同步工具相关,如信号量、闭锁、栅栏等功能;
locks
Lock 是一个类似于 synchronized 块的线程同步机制。但是 Lock 比 synchronized 块更加灵活、精细。Lock是一个接口,有一个实现类:ReentrantLock重入锁。重入锁还有公平锁和非公平锁之分,其中公平锁需要增加请求锁的时机先后的判断。
使用示例:
Lock lock = new ReentrantLock();
lock.lock();
//critical section
lock.unlock();
支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。在java关键字synchronized隐式支持重入性。支持重入性需要解决这样两个问题:
- 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功
- 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。
Lock与synchronized的区别:
- synchronized 代码块不能够保证进入访问等待的线程的先后顺序
- 不能够传递任何参数给一个 synchronized 代码块的入口, synchronized 代码块不能设置等待超时时间
- synchronized 块必须被完整地包含在单个方法里。而一个 Lock 对象可以把它的 lock() 和 unlock() 方法的调用放在不同的方法里。
另外还有一个读写锁:ReadWriteLock,读写锁是一种先进的线程锁机制。
- 允许多个线程在同一时间对某特定资源进行读取
- 但同一时间内只能有一个线程对其进行写入
实现类:ReentrantReadWriteLock,示例:
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
readWriteLock.readLock().lock();
// 可以有多个读锁,读锁的时候不能写
readWriteLock.readLock().unlock();
readWriteLock.writeLock().lock();
// 没有任何读操作或者写操作时,才可以写锁
readWriteLock.writeLock().unlock();
atomic原子性包装类
位于 atomic包下,包含一系列原子性变量。其中有一个重要的方法是:CAS(compareAndSet),它包含3个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。
仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做,并返回false。CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。但实际上,当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
那有没有可能在判断V和E相同后,正要赋值时,切换了线程,更改了值。造成了数据不一致呢?不会,CAS整一个操作过程是一个原子操作,它是由一条CPU指令完成的。这个就是它能保证并发的原理。
executor
ExecutorService 是个接口,它有很多实现类,其实现类实际上是线程池,主要有两个实现类:
- ThreadPoolExecutor
- ScheduledThreadPoolExecutor
Executors是一个线程池的工厂类,提供类很多线程池的创建方法。但是我们最好还是自己new 实现类,譬如常用的:ThreadPoolExecutor,去设置其中的参数。
使用完 ExecutorService 之后你应该将其关闭,以使其中的线程不再运行。比如,如果你的应用是通过一个 main() 方法启动的,之后 main 方法退出了你的应用,如果你的应用有一个活动的 ExexutorService 它将还会保持运行。ExecutorService 里的活动线程阻止了 JVM 的关闭。
要终止 ExecutorService 里的线程你需要调用 ExecutorService 的 shutdown() 方法。ExecutorService 并不会立即关闭,但它将不再接受新的任务,而且一旦所有线程都完成了当前任务的时候,ExecutorService 将会关闭。在 shutdown() 被调用之前所有提交给 ExecutorService 的任务都被执行。
如果你想要立即关闭 ExecutorService,你可以调用 shutdownNow() 方法。这样会立即尝试停止所有执行中的任务,并忽略掉那些已提交但尚未开始处理的任务。无法担保执行任务的正确执行。
并发容器
并发容器中比较常见的有两种:
- concurrentHashMap:使用的最多的并发容器,在容器组件模式中,经常用作通用容器
- 队列:队列这种容器似乎天生和并发有关,一般会有一个线程插入数据,一个线程读取数据,如果只有一个线程的话就没有必要使用队列了
concurrentHashmap已经有很多地方提到了,这边只着重说一下队列:
BlockingQueue:阻塞队列,是一个接口。BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。它具有如下几种实现:
- ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。内部以 FIFO(先进先出)的顺序对元素进行存储。
- DelayQueue 对元素进行持有直到一个特定的延迟到期。
- LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。线程池经常使用该队列作为线程队列
- PriorityBlockingQueue 是一个无界的并发队列。
- SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。
一个例子:
BlockingQueue queue = new ArrayBlockingQueue(1024);
// 线程一
queue.put("1");
// 线程二
queue.take()
另外还有:阻塞双端队列 BlockingDeque,deque(双端队列) 是 "Double Ended Queue" 的缩写。因此,双端队列是一个你可以从任意一端插入或者抽取元素的队列。
tools并发工具
并发工具有四种:
CountDownLatch
CountDownLatch 是一个并发构造,它允许一个或多个线程等待一系列指定操作的完成。
- CountDownLatch 以一个给定的数量初始化。countDown() 每被调用一次,这一数量就减一。
- 通过调用 await() 方法,该线程可以阻塞等待这一数量到达零。
CyclicBarrier
CyclicBarrier 类是一种同步机制,它能够对处理一些算法的线程实现同步。
参考:http://tutorials.jenkov.com/java-util-concurrent/cyclicbarrier.html
Exchanger
Exchanger 类表示一种两个线程可以进行互相交换对象的会和点。
参考:http://tutorials.jenkov.com/java-util-concurrent/exchanger.html
Semaphore
Semaphore 类是一个计数信号量。具备两个主要方法:
- acquire() 每调用一次 acquire(),一个许可会被调用线程取走。
- release() 每调用一次 release(),一个许可会被返还给信号量。
主要使用场景:
1、保护一个重要(代码)部分防止一次超过 N 个线程进入。
Semaphore semaphore = new Semaphore(1);
//critical section
semaphore.acquire();
...
semaphore.release();
2、在两个线程之间发送信号。
如果你将一个信号量用于在两个线程之间传送信号,通常你应该用一个线程调用 acquire() 方法,而另一个线程调用 release() 方法。如果没有可用的许可,acquire() 调用将会阻塞,直到一个许可被另一个线程释放出来。如果无法往信号量释放更多许可时,一个 release() 调用也会阻塞。