JUC之并发容器

这个是在学习工作中的一些总结,若有不对之处欢迎大家指出。侵删!
需要源码联系QQ:1352057131
得之在俄顷,积之在平日。

目录

并发容器概览

古老和过时的同步容器

Vector

Hashtable

ConcurrentHashMap

Map接口图示

HashMap结构

HashMap不安全的原因

同时put碰撞导致数据丢失

同时put扩容导致数据丢失

死循环造成的cpu100%(仅在1.8版本之前出现)

ConcurrentHashMap结构

JDK1.7:

ConcurrentHashMap常见用法

CopyOnWriteArrayList

背景

适用场景

读写规则

实现原理

缺点

并发队列Queue

为什么需要使用队列

队列关系图

阻塞队列BlockingQueue

简介

常用方法

ArrayBlockingQueue示例

LinkedBlockingQueue

PriorityBlockingQueue

非阻塞并发队列

 

并发容器概览

ConcurrentHashMap:线程安全的HashMap

CopyOnWriteArrayList:线程安全的List

BlockingQueue:这是一个借口,表示阻塞队列,非常适用于作为数据共享的通道

ConcurrentLinkQueue:高效的非阻塞并发队列,使用链表实现,可以看作是一个线程安全的LinkList

ConcurrentSkipListMap:是一个map,使用跳表的数据结构进行快速查找

古老和过时的同步容器

Vector

Vector可以看成是一个线程安全的list,因为底层的方法是用synchronized进行修饰了的,因此在并发的情况下执行效率并不高

JUC之并发容器_第1张图片


 

Hashtable

Hashtable可以看成是一个线程安全的HashMap,因为底层的方法是用synchronized进行修饰了的,因此在并发的情况下执行效率并不高

JUC之并发容器_第2张图片

ConcurrentHashMap

Map接口图示

JUC之并发容器_第3张图片

JUC之并发容器_第4张图片

HashMap结构

JDK1.7

JUC之并发容器_第5张图片

JUC之并发容器_第6张图片

 

 

JDK1.8

JUC之并发容器_第7张图片

JUC之并发容器_第8张图片

 

 

HashMap不安全的原因

同时put碰撞导致数据丢失

多个线程如果同时put,而且key计算出来的hashCode都相同,则会放入同一个位置,因此造成数据丢失

同时put扩容导致数据丢失

如果多个线程同时扩容,那么只会有一个扩容后的数组会被保留下来

死循环造成的cpu100%(仅在1.8版本之前出现)

原因:多个线程同时扩容的时候会造成链表的死循环,也就是你指向我,我指向你。

具体参见:https://www.jianshu.com/p/619a8efcf589

ConcurrentHashMap结构

JDK1.7:

JUC之并发容器_第9张图片

JUC之并发容器_第10张图片

Jdk1.7中的ConcurrentHashMap最外层是多个segment,每个segment底层数据结构和HashMap相似,仍然是数组和链表组成的拉链法。

每个segment独立上ReentrantLock锁,每个segment之间互不影响,提高了并发效率。

ConcurrentHashMap默认有16个segment,所以最多同时支持16个线程并发写(操作分别在不同的segment上)该默认值可以在初始化的时候指定,一旦指定就无法扩容。

JDK1.8

JUC之并发容器_第11张图片

从图中我们可以看出ConcurrentHashMap的结构和HashMap结构非常相似,1.8中ConcurrentHashMap最外层不再采用segment,而是采用node节点,保证线程安全的方式不再是ReentrantLock而是CAS+Synchronized

ConcurrentHashMap常见用法

  1. void putAll(Map m):将指定映射中所有映射关系复制到此映射中。
  2. V putIfAbsent(K key, V value):如果指定键已经不再与某个值相关联,则将它与给定值关联。
  3. V remove(Object key):从此映射中移除键(及其相应的值)。
  4. boolean remove(Object key, Object value):只有目前将键的条目映射到给定值时,才移除该键的条目。
  5. V replace(K key, V value):只有目前将键的条目映射到某一值时,才替换该键的条目。
  6. boolean replace(K key, V oldValue, V newValue):只有目前将键的条目映射到给定值时,才替换该键的条目。

CopyOnWriteArrayList

背景

java诞生之初 就有线程安全的Vector,但Vector对读写都是通过synchronized关键字来同步的,性能并不好且Vector每次扩容是原来的1倍,存在内存浪费的可能。Vector和SynchronizeList锁的粒度较大,并发时效率相对较低,并且迭代时无法编辑。对于线程安全的List JDK提供了CopyOnWriteArrayList

适用场景

  1. 读操作要快,写操作无所谓。
  2. 读多写少的场景

读写规则

读完全不用加锁,写入也不会阻塞读操作,只有写入与写入之间需要进行同步等待。

实现原理

读操作是无锁的,性能较高。写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。如下图所示:

JUC之并发容器_第12张图片

缺点

  1. 数据一致性问题,CopyOnWrite容器只能保证数据的最终一致性不能保证数的实时一致性。
  2. 内存占用问题:因为CopyOnWrite的写是复制机制,所以在写操作的时候内存会同时有两个对象。

并发队列Queue

为什么需要使用队列

用队列可以在线程间传递数据:生产者消费者模式、银行转账。

考虑到锁等线程安全问题的重任从“你”转移到了“队列”上。

队列关系图

JUC之并发容器_第13张图片

阻塞队列BlockingQueue

简介

阻塞队列是具有阻塞功能的队列,阻塞队列的一端给生产者用,另一端给消费者使用,阻塞队列是线程安全的。

JUC之并发容器_第14张图片

常用方法

放入数据

    1. offer(anObject):表示如果可能的话,将anObject加BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.(本方法不阻塞当前执行方法的线程);       
    2. offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。
    3. put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.

获取数据

  1. poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null;
  2. poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败。
  3. take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入; 
  4. drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。

是否有界

是否有界即容量有多大,无界队列的参数是Integer.MAX_VALUE。

ArrayBlockingQueue示例

public class ArrayBlockingQueueTest {
    /*模拟一个面试官,10名面试者,而只有两把椅子给面试者坐*/
    //创建一个容量为2的队列
   
private final static ArrayBlockingQueue abq = new ArrayBlockingQueue(4);
    public static void main(String[] args) {
        Interviewer interviewer = new Interviewer(abq);
        Interviewee interviewee = new Interviewee(abq);
        new Thread(interviewee).start();
        new Thread(interviewer).start();

    }
}
    //面试官
   
class Interviewer implements Runnable{
        private ArrayBlockingQueue arrayBlockingQueue;
        public Interviewer(ArrayBlockingQueue arrayBlockingQueue) {
            this.arrayBlockingQueue = arrayBlockingQueue;
        }
        @Override
       
public void run() {
            while(!arrayBlockingQueue.isEmpty()) {
                String name = arrayBlockingQueue.poll();
                System.out.println("正在面试"+name);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(name+"面试完毕");
            }
            System.out.println("无面试者");
        }
    }
    //面试者
   
class Interviewee implements Runnable{
        private ArrayBlockingQueue arrayBlockingQueue;

        public Interviewee(ArrayBlockingQueue arrayBlockingQueue) {
            this.arrayBlockingQueue = arrayBlockingQueue;
        }
        @Override
       
public void run() {
            //将8个面试者放入队列
           
System.out.println("所有的面试者都到齐了");
            for (int i = 0; i < 8; i++) {
                try {
                    arrayBlockingQueue.put("面试者"+i);
                    System.out.println("面试者"+i+"进入队列等待");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
}

运行结果:

JUC之并发容器_第15张图片

LinkedBlockingQueue

该队列是一个无界队列,容量为Integer.MAX_VALUE,内部结构是node节点和两把锁:
正在上传…重新上传取消转存失败重新上传取消
这样使用两把锁的好处是让take和put互不干扰

PriorityBlockingQueue

这是一个支持优先级的无界队列,并且可以自然排序。它是PriorityQueue的线程安全版本。

SynchronousQueue

这是一个容量为0的队列,因此都是直接传递,没有peek、iterate等方法,从而效率很高。这是一个非常好的用来直接传递的并发数据结构;SynchronousQueue是线程池Executors.newCachedThreadPool()

使用的阻塞队列。

非阻塞并发队列

并发包中非阻塞队列只有ConcurrentLinkedQueue,它是使用链表作为其数据结构,使用CAS非阻塞算法来实现线程安全,适用于对性能要求高的并发场景。

你可能感兴趣的:(JUC)