JDK并发包
JDK5之后引进了并发包java.util.concurrent,让并发的开发更加可控,更加简单。所以有必要好好学习下,下面从同步控制、并发容器、线程池三部分来详细了解它。
1. 各种同步控制工具的使用
1.1 ReentrantLock(重用锁)
1)与synchronized的区别是,它需要手动申请锁与解锁,在 finally 块中释放锁,而synchronized是JVM自动处理的。可控性上ReentrantLock更强。
由于ReentrantLock是重入锁,所以可以反复得到相同的一把锁,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放(重入锁)。
注:synchronized 也是可重入的,当线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个synchronized 块时,才释放锁,例如:
在带有锁的方法1里面调用了带有锁的方法2,这时在方法1获得的锁还是没有释放的,其它线程还是访问不了方法1,即使方法2结束了,直至方法1的锁释放,锁才会真正释放。假如有多个线程同时来调用方法1,那输出结果还是按顺序输出:1,2,1,2...
2)还有与synchronized不同的是,ReentrantLock对中断是有响应的。普通的lock.lock()是不能响应中断的,lock.lockInterruptibly()能够响应中断。
3)可限时,超时不能获得锁,就返回false,不会永久等待构成死锁,使用lock.tryLock(long timeout, TimeUnit unit)来实现可限时锁,参数为时间和单位。无法获得后就直接退出了。
1.2 Condition
Condition与ReentrantLock的关系就类似于synchronized与Object.wait()/signal()
await()方法会使当前线程等待,同时释放当前锁,当其他线程中使用signal()时或者signalAll()方法时,线 程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。这和Object.wait()方法很相似。
awaitUninterruptibly()方法与await()方法基本相同,但是它并不会再等待过程中响应中断。 singal()方法用于唤醒一个在等待中的线程。相对的singalAll()方法会唤醒所有在等待中的线程。这和Obejct.notify()方法很类似。
1.3.Semaphore
对于锁来说,它是互斥的排他的。意思就是,只要我获得了锁,没人能再获得了。
而对于Semaphore来说,它允许多个线程同时进入临界区。可以认为它是一个共享锁,但是共享的额度是有限制的,额度用完了,其他没有拿到额度的线程还是要阻塞在临界区外。当额度为1时,就相等于lock。
常用方法有:
semaphore.acquire();//申请许可,当然一个线程也可以一次申请多个许可acquire(
int
permits),谁拿到令牌(acquire)就可以去执行了,如果没有令牌则需要等待。
semaphore.release();//执行完毕,一定要归还(release)令牌,否则令牌会被很快用光,别的线程就无法获得令牌而执行下去了。
1.4 ReadWriteLock
ReadWriteLock是区分功能的锁。读和写是两种不同的功能,读-读不互斥,读-写互斥,写-写互斥。
这样的设计是并发量提高了,又保证了数据安全。
使用方式是:
private
static
ReentrantReadWriteLock readWriteLock=
new
ReentrantReadWriteLock();
private
static
Lock readLock = readWriteLock.readLock();
private
static
Lock writeLock = readWriteLock.writeLock();
1.5 CountDownLatch
倒数计时器,等待所有检查线程全部完工后,再执行。每个任务完成后调用countDown(),计数器就会减1,当计数为0的时候,await()阻塞后面的方法会继续执行。
1.6 CyclicBarrier
和CountDownLatch相似,也是等待某些线程都做完以后再执行。与CountDownLatch区别在于这个计数器可以反复使用。比如,假设我们将计数器设置为10。那么凑齐第一批1 0个线程后,计数器就会归零,然后接着凑齐下一批10个线程。并且每次完成一批线程后会触发一个动作
CyclicBarrier(
int
parties, Runnable barrierAction)//
barrierAction就是当计数器一次计数完成后,系统会执行的动作
也是通过await()来阻塞主线程等待任务全部完成
2. 并发容器
2.1 ConcurrentHashMap
我们知道HashMap不是一个线程安全的容器,最简单的方式使HashMap变成线程安全就是使用Collections.synchronizedMap,它是对HashMap的一个包装,如:
Collections.synchronizedMap(
new
HashMap())
同理对于List,Set也提供了相似方法。
但是这种方式只适合于并发量比较小的情况,它会将HashMap包装在里面,然后将HashMap的每个操作都加上synchronized。
下面来看下ConcurrentHashMap是如何实现的:
在 ConcurrentHashMap内部有一个Segment段,它将大的HashMap切分成若干个段(小的HashMap),然后让数据在每一段上Hash,这样多个线程在不同段上的Hash操作一定是线程安全的,所以只需要同步同一个段上的线程就可以了,这样实现了锁的分离,大大增加了并发量。
在使用ConcurrentHashMap.size时会比较麻烦,因为它要统计每个段的数据和,在这个时候,要把每一个段都加上锁,然后再做数据统计。这个就是把锁分离后的小小弊端,但是size方法应该是不会被高频率调用的方法。
2.2 BlockingQueue
BlockingQueue不是一个高性能的容器。但是它是一个非常好的共享数据的容器。是典型的生产者和消费者的实现。
它在内部实现了同步的队列,实现方式采用的是我们第2种await() / signal()方法。它可以在生成对象时指定容量大小。它用于阻塞操作的是put()和take()方法。
put()方法:类似于我们上面的生产者线程,容量达到最大时,自动阻塞。
take()方法:类似于我们上面的消费者线程,容量为0时,自动阻塞。
3.线程池
线程池的作用就是将线程进行复用,减少创建与销毁线程的开销。
ThreadPoolExecutor是线程池的一个重要实现。
而Executors是一个工厂类。
新提交一个任务时的处理流程很明显:
1、如果线程池的当前大小还没有达到基本大小(poolSize < corePoolSize),那么就新增加一个线程处理新提交的任务;
2、如果当前大小已经达到了基本大小,就将新提交的任务提交到阻塞队列排队,等候处理workQueue.offer(command);
3、如果队列容量已达上限,并且当前大小poolSize没有达到maximumPoolSize,那么就新增线程来处理任务;
4、如果队列已满,并且当前线程数目也已经达到上限,那么意味着线程池的处理能力已经达到了极限,此时需要拒绝新增加的任务。至于如何拒绝处理新增
的任务,取决于线程池的饱和策略RejectedExecutionHandler。
3.1.线程池的种类
- new FixedThreadPool 固定数量的线程池,线程池中的线程数量是固定的,不会改变。
- new SingleThreadExecutor 单一线程池,线程池中只有一个线程。
- new CachedThreadPool 缓存线程池,线程池中的线程数量不固定,会根据需求的大小进行改变。
- new ScheduledThreadPool 计划任务调度的线程池,用于执行计划任务,比如每隔5分钟怎么样
3.2.拒绝策略
有时候,任务非常繁重,导致系统负载太大。在上面说过,当任务量越来越大时,任务都将放到FixedThreadPool的阻塞队列中,导致内存消耗太大,最终导致内存溢出。这样的情况是应该要避免的。因此当我们发现线程数量要超过最大线程数量时,我们应该放弃一些任务。丢弃时,我们应该把任务记下来,而不是直接丢掉。
共有以上4种策略。
AbortPolicy:如果不能接受任务了,则抛出异常。
CallerRunsPolicy:如果不能接受任务了,则让调用的线程去完成。
DiscardOldestPolicy:如果不能接受任务了,则丢弃最老的一个任务,由一个队列来维护。
DiscardPolicy:如果不能接受任务了,则丢弃任务。
在ThreadPoolExecutor的另一个构造函数中,可传入一个handler,而handler就是拒绝策略的实现,它会告诉我们,如果任务不能执行了,该怎么做.
3.3 ForkJoin
Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。有点类似工作流里面的分支合并概念
参考文章:
https://my.oschina.net/hosee/blog/485121
http://www.importnew.com/21288.html
https://my.oschina.net/hosee/blog/614319
3.4 Future
Future<V>代表一个异步执行的操作,通过get()方法可以获得操作的结果,如果异步操作还没有完成,则get()会使当前线程阻塞。FutureTask<V>实现了Future<V>和Runable<V>
3.5 Callable
一个有返回值的操作
3.6 CompletionService
CompletionService对ExecutorService进行了包装,内部维护一个保存Future对象的BlockingQueue。只有当这个Future对象状态是结束的时候,才会加入到这个Queue中,take()方法其实就是Producer-Consumer中的Consumer。它会从Queue中取出Future对象,如果Queue是空的,就会阻塞在那里,直到有完成的Future对象加入到Queue中。所以,先完成的必定先被取出。这样就减少了不必要的等待时间。
与迭代了FutureTask的数组的区别是,CompletionService 任务完成后就把其结果加到result中,而不用依次等待每个任务完成。