JAVA:并发

Posted 不闻余物

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA:并发相关的知识,希望对你有一定的参考价值。


 

并发包

 

1、常用的并发集合类

  • ConcurrentHashMap:线程安全的HashMap的实现
  • CopyOnWriteArrayList:线程安全且在读操作时无锁的ArrayList
  • CopyOnWriteArraySet:基于CopyOnWriteArrayList,不添加重复元素
  • ArrayBlockingQueue:基于数组、先进先出、线程安全,可实现指定时间的阻塞读写,并且容量可以限制
  • LinkedBlockingQueue:基于链表实现,读写各用一把锁,在高并发读写操作都多的情况下,性能优于ArrayBlockingQueue

2、原子类

  • AtomicInteger:线程安全的Integer,基于CAS(无阻塞,CPU原语),优于使用同步锁的Integer

3、线程池

  • ThreadPoolExecutor:一个高效的支持并发的线程池,可以很容易的将一个实现了Runnable接口的任务放入线程池执行,但要用好这个线程池,必须合理配置corePoolSize、最大线程数、任务缓冲队列,以及队列满了+线程池满时的回绝策略,一般而言对于这些参数的配置,需考虑两类需求:高性能和缓冲执行
  • Executor:提供了一些方便的创建ThreadPoolExecutor的方法。
  • FutureTask:可用于异步获取执行结果或取消执行任务的场景,基于CAS,避免锁的使用。

4、锁

  • ReentrantLock:与synchronized效果一致,但是又更加灵活,支持公平/非公平锁、支持可中断的锁、支持非阻塞的tryLock(可超时)、支持锁条件等,需要手工释放锁,基于AbstractQueueSynchronizer
  • ReentrantReadWriteLock:与ReentrantLock没有关系,采用两把锁,用于读多写少的情形

 

参考:Java并发包类源码解析

 


 

并发集合类

 

1、ConcurrentHashMap

(Concurrent HashMap -> Segment -> HashBucket -> HashEntry)

  • 线程安全的HashMap的实现
  • 数据结构:一个指定个数的Segment数组,数组中的每一个元素Segment相当于一个HashTable(一个HashEntry[])
  • 扩容的话,只需要扩自己的Segment而非整个table扩容
  • key与value均不可以为null,而hashMap可以
  • 向map添加元素
    • 根据key获取key.hashCode的hash值
    • 根据hash值算出将要插入的Segment
    • 根据hash值与Segment中的HashEntry的容量-1按位与获取将要插入的HashEntry的index
    • 若HashEntry[index]中的HashEntry链表有与插入元素相同的key和hash值,根据onlyIfAbsent决定是否替换旧值
    • 若没有相同的key和hash,直接返回将新节点插入链头,原来的头节点设为新节点的next(采用的方式与HashMap一致,都是HashEntry替换的方法)
  • ConcurrentHashMap基于concurrencyLevel划分出多个Segment来存储key-value,这样的话put的时候只锁住当前的Segment,可以避免put的时候锁住整个map,从而减少了并发时的阻塞现象
  • 从map中获取元素
    • 根据key获取key.hashCode的hash值
    • 根据hash值与找到相应的Segment
    • 根据hash值与Segment中的HashEntry的容量-1按位与获取HashEntry的index
    • 遍历整个HashEntry[index]链表,找出hash和key与给定参数相等的HashEntry,例如e
      • 如没找到e,返回null
      • 如找到e,获取e.value
        • 如果e.value!=null,直接返回
        • 如果e.value==null,则先加锁,等并发的put操作将value设置成功后,再返回value值
  • 对于get操作而言,基本没有锁,只有当找到了e且e.value等于null,有可能是当下的这个HashEntry刚刚被创建,value属性还没有设置成功,这时候我们读到是该HashEntry的value的默认值null,所以这里加锁,等待put结束后,返回value值
  • 加锁情况(分段锁):
    • put
    • get中找到了hash与key都与指定参数相同的HashEntry,但是value==null的情况
    • remove
    • size():三次尝试后,还未成功,遍历所有Segment,分别加锁(即建立全局锁)

 参考:常用并发包并发包

 

2、CopyOnWriteArrayList

  • 线程安全且在读操作时无锁的ArrayList
  • 采用的模式就是"CopyOnWrite"(即写操作-->包括增加、删除,使用复制完成)
  • 底层数据结构是一个Object[],初始容量为0,之后每增加一个元素,容量+1,数组复制一遍。(当有新元素加入的时候,创建新数组,并往新数组中加入一个新元素,这个时候,array这个引用仍然是指向原数组的。当元素在新数组添加成功后,将array这个引用指向新数组。)
  • 遍历的只是全局数组的一个副本,即使全局数组发生了增删改变化,副本也不会变化,所以不会发生并发异常。但是,可能在遍历的过程中读到一些刚刚被删除的对象
  • 增删改上锁、读不上锁
  • 读多写少且脏数据影响不大的并发情况下,选择CopyOnWriteArrayList

3、CopyOnWriteArraySet

  • 基于CopyOnWriteArrayList,不添加重复元素
  • CopyOnWriteArraySet在add元素的时候要遍历一遍数组,从而起到不添加重复元素的作用,但是由于要遍历数组,效率也会低于CopyOnWriteArrayList的add

4、ArrayBlockingQueue

  • 基于数组、先进先出、线程安全,可实现指定时间的阻塞读写,并且容量可以限制。内部维护数组来缓冲队列中的数据对象。
  • 组成:一个对象数组+1把锁ReentrantLock+2个条件Condition
  • 三种入队对比
    • offer(E e):如果队列没满,立即返回true; 如果队列满了,立即返回false-->不阻塞
    • put(E e):如果队列满了,一直阻塞,直到数组不满了或者线程被中断-->阻塞
    • offer(E e, long timeout, TimeUnit unit):在队尾插入一个元素,,如果数组已满,则进入等待,直到出现以下三种情况:-->阻塞
      • 被唤醒
      • 等待时间超时
      • 当前线程被中断
  • 三种出队对比
    • poll():如果没有元素,直接返回null;如果有元素,出队
    • take():如果队列空了,一直阻塞,直到数组不为空或者线程被中断-->阻塞
    • poll(long timeout, TimeUnit unit):如果数组不空,出队;如果数组已空且已经超时,返回null;如果数组已空且时间未超时,则进入等待,直到出现以下三种情况:
      • 被唤醒
      • 等待时间超时
      • 当前线程被中断
  • 需要注意的是,数组是一个必须指定长度的数组,在整个过程中,数组的长度不变,队头随着出入队操作一直循环后移
  • 锁的形式有公平与非公平两种
  • 在只有入队高并发或出队高并发的情况下,因为操作数组,且不需要扩容,性能很高
  • 多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。
  • 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。这也是我们在多线程环境下,为什么需要BlockingQueue的原因。作为BlockingQueue的使用者,我们再也不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。

参考:ArrayBlockingQueue (写得很详细,推荐!)

 

5、LinkedBlockingQueue

  • 基于链表实现,读写各用一把锁,在高并发入队和出队操作都多的情况下,性能优于ArrayBlockingQueue。内部维护链表来缓冲队列中的数据对象。
  • 组成一个链表+两把锁+两个条件
  • 默认容量为整数最大值,可以看做没有容量限制
  • 三种入队与三种出队与上边完全一样,只是由于LinkedBlockingQueue的的容量无限,在入队过程中,可能根本没有阻塞等待

 


 

ReentrantLock

  • 支持公平锁(先进来的线程先执行)
  • 支持非公平锁(后进来的线程也可能先执行)

 

ReentrantLock和Synchronized

  1.synchronized

  优点:实现简单,语义清晰,便于JVM堆栈跟踪,加锁解锁过程由JVM自动控制,提供了多种优化方案,使用更广泛

  缺点:悲观的排他锁,不能进行高级功能

  2.lock

  优点:可定时的、可轮询的与可中断的锁获取操作,提供了读写锁、公平锁和非公平锁  

  缺点:需手动释放锁unlock,不适合JVM进行堆栈跟踪

  3.相同点 

  都是可重入锁

 

参考:ReentantLock和synchronized

 

以上是关于JAVA:并发的主要内容,如果未能解决你的问题,请参考以下文章

java高并发,如何解决,啥方式解决,高并发

java 怎样处理高并发

java并发编程看啥书比较好

Java并发总结-全景图

Java并发指南开篇:Java并发编程学习大纲

java中怎么实现高并发