java并发容器

看《java并发编程的艺术》,发现这一章中很多内容是基于java7甚至更早之前的jdk版本,导致一些内容已经过时了,所以这里记录一下新的java并发容器的内容,内容主要来自《java并发编程的艺术》,《实战java高并发程序设计》。

jdk提供的线程安全的并发容器大部分在java.util.concurrent包中,下面是一些常用的类,java.util中的Vector也是线程安全的,但是效率较低。LinkedList并不是线程安全的,不过可以使用Collections.synchronizedList()方法来包装。

List<String> list  = Collections.synchronizedList(new ArrayList<>());

一、ConcurrentHashMap的实现原理与使用

HashMap多线程下可能会使内部的链表成环,造成死循环,使得CPU的占用率很高,达到100%,甚至可能导致死机。一个方案是使用Collections.synchronizedMap()方法进行包装。

Map<String, String> m = Collections.synchronizedMap(new HashMap<>());

但这样做无论是数据的写入还是读取,都需要获得mutex锁,这样在多线程情况下并发度不高。

还有一个方案是使用ConcurrentHashMap代替HashMap。ConcurrentHashMap使用上难度不大,但要注意ConcurrentHashMap不允许key或value为null,这个博客总结得不错https://blog.csdn.net/u010723709/article/details/48007881#commentBox

https://blog.csdn.net/weixin_44460333/article/details/86770169  https://blog.csdn.net/bill_xiang_/article/details/81122044

ConcurrentHashMap 1.7:是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。ConcurrentHashMap 1.8:1.7版的问题是查询遍历链表效率太低。其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。其中的 val next 都用了 volatile 修饰,保证了可见性。

面试5连:

  1. 谈谈你理解的 HashMap,讲讲其中的 get put 过程。

  2. 1.8 做了什么优化?

  3. 是线程安全的嘛?

  4. 不安全会导致哪些问题?

  5. 如何解决?有没有线程安全的并发容器?

  6. ConcurrentHashMap 是如何实现的? 1.7、1.8 实现有何不同?为什么这么做?

二、ConcurrentLinkQueue

1. CLQ简介

https://blog.csdn.net/qq_38293564/article/details/80798310 前面几段

2. offer过程简介(结合着上链接文和下文),这么复杂的原因

3. put过程简介

4. 其他方法

ConcurrentLinkedQueue类应该算是在高并发环境中性能最好的队列了,它之所以有很好的性能,是因为它内部复杂的实现。 

其内部有一个静态内部类Node,其部分源码如下所示:

    private static class Node<E> {
        volatile E item;
        volatile Node<E> next;

 item用来存放其目标元素,比如泛型定义该队列为String时,item就是String类型。

对Node进行操作,使用了CAS

关于head和tail节点的说明

进队列方法源码如下所示:

p.casNext(null,newNode),判断p的next是否为null,为null则将newNode设置为next,否则设置失败

casTail(t,newNode),判断t是否指向tail,为true则将更新tail节点,将tail指向新节点

第十七行是判断是否是哨兵节点,哨兵节点是next指向自己的节点

public boolean offer(E e) {
        checkNotNull(e);
        final Node<E> newNode = new Node<E>(e);

        for (Node<E> t = tail, p = t;;) {
            Node<E> q = p.next;
            if (q == null) {
                // p is last node
                if (p.casNext(null, newNode)) {
                    // Successful CAS is the linearization point
                    // for e to become an element of this queue,
                    // and for newNode to become "live".
                    if (p != t) // hop two nodes at a time
                        casTail(t, newNode);  // Failure is OK.
                    return true;
                }
                // Lost CAS race to another thread; re-read next
            }
            else if (p == q)
                // We have fallen off list.  If tail is unchanged, it
                // will also be off-list, in which case we need to
                // jump to head, from which all live nodes are always
                // reachable.  Else the new tail is a better bet.
                p = (t != (t = tail)) ? t : head;
            else
                // Check for tail updates after two hops.
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

<<实战java高并发程序设计》中有解释,搬运一下:

 哨兵如何产生以及poll方法:

定义一个ConcurrentLinkedQUeue对象

poll()函数源码:

   public E poll() {
        restartFromHead:
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
                E item = p.item;

                if (item != null && p.casItem(item, null)) {
                    // Successful CAS is the linearization point
                    // for item to be removed from this queue.
                    if (p != h) // hop two nodes at a time
                        updateHead(h, ((q = p.next) != null) ? q : p);
                    return item;
                }
                else if ((q = p.next) == null) {
                    updateHead(h, p);
                    return null;
                }
                else if (p == q)
                    continue restartFromHead;
                else
                    p = q;
            }
        }
    }

《实战java高并发程序设计中的解释 》中哨兵的解释:

 ConcurrentLinkedQueue的其他方法:

  peek():获取表头元素但不移除队列的头,如果队列为空则返回null。

  remove(Object obj):移除队列已存在的元素,返回true,如果元素不存在,返回false。

  add(E e):将指定元素插入队列末尾,成功返回true,失败返回false(此方法非线程安全的方法,不推荐使用)。

注意:

  虽然ConcurrentLinkedQueue的性能很好,但是在调用size()方法的时候,会遍历一遍集合,对性能损害较大,执行很慢,因此应该尽量的减少使用这个方法,如果判断是否为空,最好用isEmpty()方法。

  ConcurrentLinkedQueue不允许插入null元素,会抛出空指针异常。

  ConcurrentLinkedQueue是无界的,所以使用时,一定要注意内存溢出的问题。即对并发不是很大中等的情况下使用,不然占用内存过多或者溢出,对程序的性能影响很大,甚至是致命的。

 

通过以上这些说明,可以明显的感觉到,不使用锁而单纯的使用CAS操作要求在应用层面上保证线程安全,并处理一些可能存在的不一致问题,大大增加了程序的设计和实现的难度。但是它带来的好处就是可以得到性能的飞速提升。因此,有些场合也是值得的。

 

我有一个问题,一直没想明白(现在想明白了):

ConcurrentLinkedQueue中offer(E e)方法中执行到最后一个分支时p = (p != t && t != (t = tail)) ? t : q;p一定是等于t的吧,那么p!=t为false,&&之后的部分不就被逻辑短路了吗,那之后的代码有什么意义呢难道如果其他线程改变了tail,t就不等于p了?

这个问题是我傻了,for循环中第一个第一个初始化条件只在第一次执行时有效,后面就不执行了,&&后面的部分执行的情况是其他线程修改了tail,q不是null,也不等于q(非哨兵),进入最后一个分支,p已经改变(在前一次执行这个语句时),这时&&后面的代码可以很快的找到最后一个节点。

另一个问题:

这么复杂的操作,是为了什么:为了少执行casTail()操作,可以提升效率。

三、BlockingQueue

(网上的原理解释不多,估计考的不多)BlockingQueue是一种特殊的队列,支持阻塞的插入和阻塞的移出。阻塞的插入是指当队列满时,队列会阻塞插入元素的线程,直到队列不满。阻塞的移出是指当队列为空时,队列会阻塞移出元素的线程,直到队列不空。put,take,offer,poll,add,remove都可以对阻塞队列进行操作,下面是阻塞队列为空或为满时各个方法的处理方式:

BlockingQueue是一个接口,它的主要实现有下面一些:

(1) ArrayBlockingQueue时一个用数组实现的有界阻塞队列。次队列按照先进先出的原则对元素进行排序。默认情况下,不保证线程访问的公平性。但也可以在定义阻塞队列的实例时传入参数,将其定义为公平的阻塞队列。

(2)LinkedBlockingQueue时一个用链表实现的无界阻塞队列。此队列的默认最大长度为Integer.MAX_VALUE(约21亿多),此队列按照先进先出的原则对元素进行排序。

(3)PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采取自然顺序排列,当然也可以自定义排序规则。

(4)DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。

 队列中的元素必须实现Delayed接口,实现Delayed接口要覆写getDelay()方法和compareTo()方法,getDelay()方法返回当前元素还需要多长时间才能从队列中返回。CompareTo()方法用于延迟队列内部排序。

延时阻塞队列的实现比较简单,当消费者从队列里获取元素时,如果元素没有达到延迟时间,就阻塞当前线程。

一个DelayQueue的例子:https://blog.csdn.net/toocruel/article/details/82769595

(5)SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作,必须等待一个take操作,否则不能继续添加元素。而且,take和put不能是同一个线程,put方法执行后,put线程会休眠,等待另一个线程来take。

一个好玩的小例子:下面代码无法停止,因为主线程再执行了put("a")后,会休眠,下面的线程无法开始执行。

import java.util.concurrent.SynchronousQueue;

public class JustTry {
    public static void main(String[] args) throws InterruptedException {
        SynchronousQueue<String> queue = new SynchronousQueue<>();
        System.out.println("put");
        queue.put("a");
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("take");
                    String str = queue.take();
                    System.out.println(str);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"thread-1").start();
    }
}

而下面的代码可以成果执行,因为take线程在尝试获得元素,一旦主线程put(),take线程会执行take操作,然后主线程也不再休眠,程序结束。 当然,最好是用两个线程,一个用来put,一个用来get。

import java.util.concurrent.SynchronousQueue;

public class JustTry {
    public static void main(String[] args) throws InterruptedException {
        SynchronousQueue<String> queue = new SynchronousQueue<>();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("take");
                    String str = queue.take();
                    System.out.println(str);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"thread-1").start();

        System.out.println("put");
        queue.put("a");


    }
}

 Synchronous默认是非公平的,但也可以在初始化时设置为公平的。

(6)LinkedTransferQueue 

LinkedTransferQueue是一个由链表结构组成的无界阻塞队列。相对于其他阻塞队列,它多了tryTransfer和transfer方法。

transfer(E e)方法:

tryTransfer(E e)方法:

tryTransfer(E e,long timeout,TimeUnit unit)方法

(7)LinkedBlockingDeque

ArrayBlockingQueue的原理:

 

四、Fork/Join框架

1.什么是Fork/Join框架:

Fork/join框架是一个用于并行执行任务的框架,是一个把大人物分割成若干个小任务,最总汇总每个小人物结果后得到大任务结果的框架。Fork就是把一个大人物划分成若干个小任务,join等待所有子任务完成,合并这些子任务的结果。

2. Fork/Join框架设计

要实现Fork/Join框架,必须实现分割任务和执行任务并合并结果这两件事情。Fork/Join框架使用两个类来完成这件事情,

ForkJoinTask类,它有两个子类,RecursiveAction:用于没有返回结果的任务,RecursiveTask:用于有返回结果的任务,我们实现自己的Fork/Join任务,如果任务有返回结果,继承RecursiveTask,否则继承RecursiveAction。

ForkJoinPool类:在实际使用中,如果毫无顾忌地使用fork()方法开启线程进行处理,很有可能导致系统开启过多的线程而影响性能。所以JDK给出了一个ForkJoinPoll线程池,对于fork()方法不着急开启线程,而是交给ForkJoinPoll线程池进行处理,以节省系统资源。

Fork/Join框架还是用了工作窃取算法:

3. 使用forkJoin框架,重点是覆写compute()方法,ForkJoinTask提供了isCompletedAbnormally()方法来检查任务是否已经抛出异常或者已经被取消,getEception方法返回Throwable对象,如果任务被取消了则返回CancellationException。如果任务没有完成或者没抛出异常则返回null。下面是一个例子:

import java.util.concurrent.*;

/**
 * ����������
 * 
 * @author tengfei.fangtf
 * @version $Id: CountTask.java, v 0.1 2015-8-1 ����12:00:29 tengfei.fangtf Exp $
 */
public class CountTask extends RecursiveTask<Integer> {

    private static final int THRESHOLD = 2; // ��ֵ
    private int              start;
    private int              end;

    public CountTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;

        // ��������㹻С�ͼ�������
        boolean canCompute = (end - start) <= THRESHOLD;
        if (canCompute) {
            for (int i = start; i <= end; i++) {
                sum += i;
            }
        } else {
            // ������������ֵ���ͷ��ѳ��������������
            int middle = (start + end) / 2;
            CountTask leftTask = new CountTask(start, middle);
            CountTask rightTask = new CountTask(middle + 1, end);
            //ִ��������
            leftTask.fork();
            rightTask.fork();
            //�ȴ�������ִ���꣬���õ�����
            int leftResult = leftTask.join();
            int rightResult = rightTask.join();
            //�ϲ�������
            sum = leftResult + rightResult;
        }
        return sum;
    }

    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        // ����һ���������?������1+2+3+4
        CountTask task = new CountTask(1, 4);
        // ִ��һ������

        ForkJoinTask<Integer> result = forkJoinPool.submit(task);
        //检查中断
        if(result.isCompletedAbnormally()){
            System.out.println(task.getException());
        }
        try {
            System.out.println(result.get());
        } catch (InterruptedException e) {
        } catch (ExecutionException e) {
        }
    }

}

 

五、CAS及ABA问题

六、跳表 

©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值