高并发编程常用容器和J.U.C包常用工具介绍

4 篇文章 0 订阅
2 篇文章 0 订阅

一、HashMap:

(一)首先是JDK1.7中HashMap的特点:
(1)存储结构为table(即数组加链表);
(2)如果要存入一个<key,value>,首先对"key"进行hash计算,对计算得出的hash值按数组长度取模,得到一个index下标(如果得出的hash值相同,则存入到相应的链表下);按index存入table结构,在存入链表时,判断链表中是否有"key"相同的节点,若存在则覆盖值;不存在,则存入链表头部;
(3)HashMap的table结构中数组长度为12(16*0.75),为防止存入的链表变得太长,减慢查询的效率,需要对数组进行扩容;
(4)扩容需要满足两个条件:存放新值的时候当前已有元素的个数必须大于等于阈值;存放新值的时候当前存放数据发生hash碰撞;
(5)扩容时把数组扩大为原来的2倍,但由于数组扩大会导致原本存在数据的index会失效;所以每次扩容时,把所有元素重新hash,把新hash值按新数组长度取模来保证index有效。
(二)接着是JDK1.8中HashMap的特点:
(1)存储结构也是table,但是当链表长度超过8时,链表会变成红黑树结构(红黑树结构在查询可以更有效率);
(2)扩容需要满足一个条件:当前存放新值(注意不是替换已有元素位置时)的时候已有元素的个数大于等于阈值(已有元素等于阈值,下一个存放后必然触发扩容机制)。
需要注意的是,HashMap是一种线程不安全的结构,为此在多线程环境下,可以用ConcurrentHashMap。

二、ConcurrentHashMap:

(一)首先是JDKJDK1.7中ConcurrentHashMap的特点:
(1)存储结构利用了“分段锁”的概念(Segment),在每个Segment中都是线程安全的;
(2)在存入一个<key,value>时,首先对"key"进行hash计算,把hash值再经过计算(可认为是类似hash计算的方式)得出下标;根据得出的下标找到放入哪个Segment(所以Segment的个数决定了并发度);在每个Segment下,是一个table(数组加链表),此table的结构和对table的操作与JDK1.7的HashMap存储结构一样,但是要加上Synchronized关键字以保证线程安全(即每个Segment下有一个锁保证线程安全);
(3)在初始化ConcurrentHashMap时需要指定Segment数量初始值(限定了并发度),一旦指定,Segment的数量就不能改变;对应每个Segment下的table结构的扩容方式和JDK1.7的HashMap相同。
(二)接着是JDKJDK1.8中ConcurrentHashMap的特点:
(1)存储结构抛弃了Segment的方式,因为这样锁的粒度比较大,在并发度高的情况下效率不高;JDK1.8中ConcurrentHashMap利用CAS和Synchronized保证了线程安全,利用红黑树提高查询效率;
(2)存储结构与JDK1.8中HashMap类似,也是table结构(数组加链表或红黑树),在存入一个<key,value>时,对"key"进行hash计算,对hash值取模,得到下标;按照下标通过CAS操作存入数组,CAS新值为"value",旧值为null,所以同一时间对于下标相同的entry来说,只有一个线程可以存入成功,其他线程会存入链表,存入链表的操作加了Synchronized关键字以保证线程安全(即在数组的每个部分都加了锁,细化了锁的粒度,提高了并发效率);
(3)同样的类似JDKJDK1.8中的HashMap,当链表长度超过8时,链表会变成红黑树结构;
(4)当原有元素个数超过阈值时,ConcurrentHashMap开始扩容,当节点所在链表的元素个数达到了阈值 8,且数组长度n小于阈值(默认是64),则会把数组长度扩大到原来的两倍(还没转换为红黑树,而是通过扩大数组长度缓解链表过长的压力),并重新调整节点位置;如果此时有线程来取数据发现扩容没有完成,数据的位置被移走了,那么就等到扩容完成才能得到数据;如果此时又有另外的线程过来存入数据,发现扩容还没有完成,则这些线程无法存入数据,而是先利用这些线程先来帮助这个ConcurrentHashMap一起扩容;把要扩容的ConcurrentHashMap分成几个部分(具体几个部分根据CPU核心数来定);
(5)扩容时涉及到一个ForwardingNode的概念,它用来标记已处理的节点或者空节点,在一个线程在自己处理的数组的区间内时,它会从这个区间的最后往前"推进",为的是去遍历此数组下标下的链表,从而重新调整数据的位置;而当遇到之前已处理的节点或者空节点时,就把这个下标标记为ForwardingNode(类似于占位符),以说明这边已经被处理过了,就继续"推进",知道所有的数据都重新调整完毕,即扩容结束。
【注】关于红黑树的理解可以看看这篇博客:30张图带你彻底理解红黑树

三、List、Set和Queue:

(一)List:
(1)ArrayList:数据可重复,有顺序,底层基于数组,每次扩容变为之前的1.5倍,它是线程不安全的;
(2)CopyOnWriteArrayList:基于ArrayList结构存数据,利用CopyOnWrite机制保证线程安全,在读操作时是读一个array引用(指向ArrayList),在写操作时则复制一个完全一样的ArrayList1,对ArrayList1进行写操作,写完成后把array引用指向ArrayList1,这样就保证了写的线程安全;CopyOnWriteArrayList适用于读极多,写极少的场景,否则会因为复制ArrayList而占用大量内存;
(3)HashSet:数据不重复,无顺序,基于HashMap实现(利用它的"key"),线程不安全;
(4)ConcurrentSkipListSet:基于ConcurrentSkipListMap实现,线程安全且查询效率比较高;在ConcurrentSkipListMap中,有一个headIndex引用作为整个查询和存储的入口,headIndex指向一个索引(包括level(这个级别永远最高)和node指针(可以是下级的所有或者真实存数据的node));真实存放数据的是一个node链表的结构,插入数据是会排好顺序插入,对于这些node节点,会创建一些索引,同等level的索引在同一层,level越小的越往下,node指针越大的越往右;这样在查询时可以直接根据此创建的索引去查找真实数据,而跳过那些比此索引小的真实数据,所以通常称为“跳表”;
(5)LinkedBlockingQueue:阻塞队列,线程安全的,特点是先进先出;如果队列已满,则放入队列时可阻塞直到有空间(put()方法);如果队列为空,则弹出队列时可阻塞直到有值(take()方法);offer()方法是添加元素,没空间直接返回false;peek()方法是返回元素,为空直接返回null;初始化时不指定长度默认无限长(比较危险);
(6)SynchronizedQueue:可以认为此队列容量始终为0,所以在put()时始终会阻塞,但当一个take()或poll()方法时,put()的元素会被取走;同样的,take()时也会阻塞,但当一个put()或offer()方法是,take()也会取走元素;使用场景是在线程池的参数中(即“来多少个线程我就直接拿多少个线程”)。

三、几个并发工具类:

包括CountDownLatch(“点火器”)、Semaphore(“信号量”)、CyclicBarrier(“摩天轮”)和FutureTask(“线程通信的传话人”)的使用示例,代码如下:

public class JUCTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /**
         * CountDownLatch点火器测试(预期:有倒计时和运动员一起开跑)
         */
        CountDownLatch countDownLatch1 = new CountDownLatch(10);
        CountDownLatch countDownLatch2 = new CountDownLatch(1);
        /**
         * CountDownLatch测试场景一:裁判员发令枪开跑倒计时
         */
        for (int i = 10; i >= 1; i--) {
            int k = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    countDownLatch1.countDown();//每次countDownLatch减1
                    System.out.println("倒数:" + k + "......\n");
                }
            }).start();
            Thread.sleep(1000);
        }
        countDownLatch1.await();//没减到0就一直阻塞
        System.out.println("开跑!!!");
        /**
         * CountDownLatch测试场景二:运动员起跑发令枪
         */
        for (int i = 0;i<=3;i++) {
            int k = i;
            new Thread(new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    countDownLatch2.await();
                    System.out.println("运动员"+k+"开跑!\n");
                }
            }).start();
        }
        Thread.sleep(1000);
        countDownLatch2.countDown();
        /**
         * Semaphore信号量测试(预期:1000个线程不会一起出来而是3个3个打印出来)
         */
        Semaphore semaphore = new Semaphore(3);
        for (int i=0;i<=30;i++){
            int k = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        semaphore.acquire();//拿一个信号量
                        System.out.println("线程"+k+"......\n");
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        semaphore.release();//释放信号量
                    }
                }
            }).start();
        }
        /**
         * CyclicBarrier摩天轮测试(4个4个的上摩天轮)
         */
        CyclicBarrier cyclicBarrier = new CyclicBarrier(4);
        for (int i=1;i<=20;i++){
            int k = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        cyclicBarrier.await();//等到有4个线程时才会往下走
                        System.out.println("线程"+k+"上摩天轮......\n");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
            Thread.sleep(1000);
        }
        /**
         * Future测试
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 5,
                3, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(3));
        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "这是" + Thread.currentThread().getName() + "线程的返回值";
            }
        };
        threadPoolExecutor.submit(callable);//提交Callable任务(一个线程在执行任务)
        FutureTask<String> future = new FutureTask<>(callable);//用FutureTask去接收Callable任务的返回值
        threadPoolExecutor.submit(future);//启动另一个线程执行
        System.out.println("这是" + Thread.currentThread().getName() + "线程获取到的结果,为:" + future.get());
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值