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

  • 相关阅读:
    tar解压出错
    HUNNU11352:Digit Solitaire
    cocos2d-x 二进制文件的读写
    电子支付概述(1)
    新一批思科电子书下载
    HUNNU11354:Is the Name of This Problem
    POJ 3181 Dollar Dayz 简单DP
    Spring中IOC和AOP的详细解释
    atheros wifi 动因分析
    Android ActionBar相关
  • 原文地址:https://www.cnblogs.com/ptw-share/p/6681353.html
Copyright © 2020-2023  润新知