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中,而不用依次等待每个任务完成。

JDK并发包的更多相关文章

  1. Java 并发编程实践基础 读书笔记: 第三章 使用 JDK 并发包构建程序

    一,JDK并发包实际上就是指java.util.concurrent包里面的那些类和接口等 主要分为以下几类: 1,原子量:2,并发集合:3,同步器:4,可重入锁:5,线程池 二,原子量 原子变量主要 ...

  2. Java并发程序设计(四)JDK并发包之同步控制

    JDK并发包之同步控制 一.重入锁 重入锁使用java.util.concurrent.locks.ReentrantLock来实现.示例代码如下: public class TryReentrant ...

  3. Java多线程--JDK并发包(2)

    Java多线程--JDK并发包(2) 线程池 在使用线程池后,创建线程变成了从线程池里获得空闲线程,关闭线程变成了将线程归坏给线程池. JDK有一套Executor框架,大概包括Executor.Ex ...

  4. Java多线程--JDK并发包(1)

    Java多线程--JDK并发包(1) 之前介绍了synchronized关键字,它决定了额一个线程是否可以进入临界区:还有Object类的wait()和notify()方法,起到线程等待和唤醒作用.s ...

  5. 3 JDK并发包

    JDK内部提供了大量实用的API和框架.本章主要介绍这些JDK内部功能,主要分为3大部分: 首先,介绍有关同步控制的工具,之前介绍的synchronized就是一种同步控制手段,将介绍更加丰富的多线程 ...

  6. 第3章 JDK并发包(三)

    3.2 线程复用:线程池 一种最为简单的线程创建和回收的方法类似如下代码: new Thread(new Runnable() { @Override public void run() { // d ...

  7. JDK并发包二

    JDK并发包二 线程复用--线程池 在线程池中,总有那么几个活跃的线程,当程序需要线程时可以从池子中随便拿一个控线程,当程序执行完毕,线程不关闭,而是将这个线程退会到池子,等待使用. JDK提供了一套 ...

  8. JDK并发包一

    JDK并发包一 同步控制是并发程序必不可少的重要手段,synchronized是一种最简单的控制方法.但JDK提供了更加强大的方法----重入锁 重入锁 重入锁使用java.util.concurre ...

  9. 第3章 JDK并发包(五)

    3.3 不要重复发明轮子:JDK的并发容器 3.3.1 超好用的工具类:并发集合简介 JDK提供的这些容器大部分在java.util.concurrent包中. ConcurrentHashMap:这 ...

  10. java多线程:jdk并发包的总结(转载)

    转载地址:http://blog.csdn.net/yangbutao/article/details/8479520 1.java 高并发包所采用的几个机制(CAS,volatile,抽象队列同步) ...

随机推荐

  1. CentOS 6.8下安装docker并使用

    Docker是一个开源的应用容器引擎,可以轻松的为任何应用创建一个轻量级的.可移植的.自给自足的容器.利用Linux的LXC.AUFS.Go语言.cgroup实现了资源的独立,可以很轻松的实现文件.资 ...

  2. C#网络程序设计(2)Socket基础编程

        本节介绍如何使用基础Socket实现TCP通信.     (1)Socket详细介绍: Socket的英文原义是"孔"或"插座".通常称作"套 ...

  3. bzoj1487 [HNOI2009]无归岛

    Description Neverland是个神奇的地方,它由一些岛屿环形排列组成,每个岛上都生活着之中与众不同的物种.但是这些物种都有一个共同的生活习性:对于同一个岛 上的任意两个生物,他们有且仅有 ...

  4. Android 窗口全屏

    全屏getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); 取消全屏 getWindow().clearFlags(Wind ...

  5. Linux添加硬盘和挂载

    1.使用fdisk -l 查看硬盘的详细信息 分析: 2.分区初始化 fdisk /dev/sdb 分析:各个参数的解析                   1. 输入 m 显示所有命令列示. 2. ...

  6. 关于vector push_back()与其他方式读取数据的效率对比

    引言: 在读取大量数据(数组)时,使用vector会尽量保证不会炸空间(MLE),但是相比于scanf的读取方式会慢上不少.但到底效率相差有多大,我们将通过对比测试得到结果. 测试数据:利用srand ...

  7. python3 selenium 切换窗口的几种方法

    第一种方法: 使用场景: 打开多个窗口,需要定位到新打开的窗口 使用方法: # 获取打开的多个窗口句柄windows = driver.window_handles# 切换到当前最新打开的窗口driv ...

  8. python基本语法-函数与异常

    # -*- coding: utf-8 -*- #自定义函数 ''' def functionname( parameters ): "函数_文档字符串" function_sui ...

  9. centos下搭建redis集群

    必备的工具: redis-3.0.0.tar redis-3.0.0.gem   (ruby和redis接口) 分析:     首先,集群数需要基数,这里搭建一个简单的redis集群(6个redis实 ...

  10. zabbix常见问题整理 持续更新……

    [toc] 1.zabbix仪表板错误 问题: zabbix server is not running: the information displayed may not be current 解 ...