并发容器

在Java常见的容器中,比如ArrayList,HashMap,HashSet等都没有考虑到线程并发的安全问题,所以用来储存并发的线程是十分不安全的。例如用HashMap ,put去储存线程,产生死循环。会造成CPU使用率100%的严重后果。因为HashMap的Entry会产生环形链表的结构。你可能又会想到HashTabe这个容器,确实它是线程安全的,但是你深入HashTable的源码会发现,它的方法是用synchronized去修饰的,这样会造成在高并发线程(数量多)的情况下,效率会非常非常低。因为在synchronized修饰的原因下,put储存线程的同时,其它线程即不能put也不能get。

1.HashMap在高并发下为什么会产生死锁?

首先看下HashMap的put方法

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) { //374行
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

中,有一个e.next也就是指针往下移动,这里就很容易出现问题了。假如线程1是从1到2遍历,线程2是从2到1遍历,那么就会形成一个死锁的循环。
在这里插入图片描述

2.位运算知识点回顾

位与,位非,位或,位异或
在这里插入图片描述
在int型上,第31位上的是符号位,31位上为1则为负,31位上为0则为负

如果移动的位数超过了该类型的最大位数,那么编译器会对移动的位数取模。如对int型移动33位,实际上只移动了332=1位。
在这里插入图片描述
负数左移动低位补0,右移动高位补1
在这里插入图片描述
正数地位补0,高位也补0
在这里插入图片描述
a%b=a&(b-1)

在这里插入图片描述

在ConcurrentHashMap有大量的位运算操作,因为这样比纯粹的取模,乘除操作符号更快。

在程序设计上,权限基本都是采用了位运算的设计,这样的权限增删改查十分迅速。

public class Action   {
    //本质上就是给了4字节的二进制位置,1代表有,0代表没有,每个位置都有其所代表的权限
    //分别赋予权限二进制参数
    public static final int ALLOW_SELECT=1<<0; //0001
    public static final int ALLOW_INSERT=1<<1; //0010
    public static final int ALLOW_UPDATE=1<<2;//0100
    public static final int ALLOW_DELETE=1<<3;//1000

    //用于存放用户现在的权限
    private int flag;

    //增加权限的方法
    public void add(int per){
        //相当于做了加法
        flag=flag|per;
    }

    //删除权限的方法
    public void delete(int per){
        //相当于做了减法
        flag=flag&~per;
    }

    //判断是否有某个权限
    public boolean isAllow(int per){
        //相当于判断了第几个位置上是否存在1,存在也就代表有权限,全有也就1111
        return (flag&per)==per;
    }

    //判断用户是否没有某个权限
    public boolean isNotAllow(int per){
        return (flag&per)==0;
    }

}

位运算除了在计算上非常快,其在属性的替换上也非常迅捷,如果每个权限你都保存的是字符那么显得非常臃肿,造成了多个对象堆积在内存。位运算就显得十分必要,一个Int类型就可以代替至少32个属性。连JDK常用的public,private等等也都是通过位运算的方式。

在这里插入图片描述

3.ConcurrentHashMap

HashTable也是线程安全的,为什么在高并发情况下被弃用了呢?查看源码可以发现,HashTable运用的锁是sycn关键字,这就造成了,在某一时间段内只有一个锁享受对容器的操作权限,其余线程都处于等待状态。
ConncurrentHashMap如何解决在保证线程安全下,还能保证不影响高并发下的容器性能的呢?
它采用了锁分段技术,也就是将容器分为很多段,每个段都有锁,这样确保了每个段是线程安全的,但是在每个段操作的同时,并不影响其它线程对其它段的操作。

JDK1.7前实现原理图
JDK 1.7里Segment是不会扩容的,初始化会给一个大小,然后一直是这个大小,table在数据增多到限定容量的时候,是会进行扩容的,扩容后会进行再次的hashCode。也就是Rehash()方法。
在这里插入图片描述
查看源码不难发现Segment是一个ReentrantLock锁,它在完成类似数组索引任务的同时又去完成了一个分段锁的功能。这个索引去找到Map集合里的数组索引,这些数组索引又去指向键值对储存的Entry,当你使用put()方法的时候,SegmentShift和SegmentMask进行位运算,快速算的算出应该存往哪个segment段里。
在这里插入图片描述

JDK1.7里ConncurrentHashMap的get()方法是如何实现的?
(1)定位segement:key的hashCode进行再散列的高位 取模
(2)定位table:key的hashCode进行再散列 取模
(3)以此扫描链表,取出想要的值

JDK1.7里ConncurrentHashMap里的size()方法是如何实现的?
size()方法首先为了保证性能是不会去获取锁(获取锁会影响Put和get的执行),如果两次结果一致,那么返回结果。如果不一致会进行全面的segement加锁,停止所有的put和get操作。所以在日常开发过程中,尽可能不去使用size()方法,是十分影响性能的。

JDK1.7里ConncurrentHashMap是存在弱一致性的
也就说,在某一时间段内你put了一个值,但是此时你去get可能获取不到这个值。

JDK1.8 ConcurrentHashMap的实现
在这里插入图片描述
在JDK1.8中取消了1.7里的Segement,采用Node数组+链表+红黑树的设计,为了保证现成的安全采用的是CAS+Sycn关键字。所以锁的粒度更小。在解决散列冲突的办法中,将拉链法中的链表更新为了链表+红黑树。至于是链表还是红黑树,取决于数据量的大小。当数量超过8的时候,链表会转为红黑树。(链表的时间复杂度是O(N),红黑树的时间复杂度是O(logN),在效率上更快,但是当数据量较小的时候就不值得用红黑树,因为红黑树的实现较为复杂,在元素的插入所需时间上,远远超过链表)

JDK1.7中key与Value的存放交由HashEntry,在JDK1.8中,key与value的存放交给了Node类。
当使用红黑树后,会变成Node的子类TreeNode类去储存。
在这里插入图片描述

3.ConcurrentHashMap

查看源码发现,ConcurrentHashMap和HashMap都是继承于AbstractMao类,所以在部分方法上还是大同小异的,但是ConncurrentHashMap增加了几个非常实用的方法,例如:putifAbsent()这个方法主要是表示,存放的时候查看散列里是否已经存在,如果存在,则不再次存入,使用旧的去充当返回结果。
在这里插入图片描述
在这里插入图片描述

4.ConcurrentSkipListMap和ConcurrentSkipListSet

ConncurrentSkipListMap和ConcurrentSkipListSet是TreeMap和TreeSet的线程安全并发版本,在JDK1.8后将其从红黑树改为了跳表结构。

什么是跳表呢?理论上红黑树的时间复杂度更低,为什么要切换跳表结构呢?
在这里插入图片描述

跳表是一种空间换时间的结构,内存利用率极低,一般为10%左右。其设计思路是建立多重索引,索引的粒度是越往下越小的。以达到快速定位查找的数所在位置的目的。在Redis中索引就是采用了跳表结构。
理论上红黑树的时间复杂度更低,为什么还要采用跳表结构呢?(1)红黑树的实现极其复杂,包括左旋右旋等。而跳表的实现简单。(2)红黑树每次增加删除都可能会打乱树的结构,需要再重新进行构造,而跳表直接进行更改头尾指针就可以。(3)红黑树一个节点存在两个指针,跳表的每个节点指针数目为(1/1-p)。跟参数相关,在参数低的情况下指针数目是少于红黑树的,例如redis参数为1/4。每个节点含有的节点个数是1.35。

5.写时复制容器 CopyOnWriteArrayList和CopyOnWriteArraySet

写时复制容器,当往容器写入数据的时候,首先会进行复制一份,在复制的那份中进行写入。读还是在原始容器。当写入完成后,读的指针地址会变更为指向写。但写时复制容器一致性差,只能保证最终一致性,因为在最终前,读和写是两个容器。所以无法保证实时一致性。
在现实应用中,主要运用于读多写少的容器。例如企业常常应用于白名单和黑名单。在每个应用中都会有关键词是不允许你进行搜索和发表的。这些不允许搜索的关键字就会放在一个黑名单里,这个关键词不是常常需要更新写入的。但是需要实时去读。所以用写时复制容器。
在这里插入图片描述

优点:(1)读写分离。
缺点:(1)占用内存,因为每次写入都要复制一下。(2)数据实时一致性无法保证。

读写锁和写时复制的性能?
写时复制的时间复杂度更低,是一种空间换时间的思想。读和写是俩个内存空间是互相不影响的。读写锁是在一个空间,读锁获取时,写锁必须等待。写锁获取时读锁必须等待。是互斥的。所以性能不如写时复制。关于读写锁的详解,笔者推荐https://www.cnblogs.com/xiaoxi/p/9140541.html

6.阻塞队列

阻塞队列,当队列为空的时候阻塞取的线程,当队列满的时候,阻塞存的线程。

阻塞队列常见于生产者-消费者模式,这种模式下存在的问题是,消费者和生产者的速率不匹配,那么为了一个它们在一个速率上,就可以让俩者通过阻塞队列去传递数据。这样保证生产快了会被阻塞,消费的快了也会被阻塞。

常用方法
在这里插入图片描述

JDK中JDK为我们提供了实现阻塞队列的标准接口BlockingQueue
在这里插入图片描述

具体的实现类有
在这里插入图片描述
有界和无界的定义?
有界和无界是相对于是否给链表或数组的长度设定大小。无界只是说不设置大小,但其也会收到计算机硬件性能的限制。

LinkedBlockingQueue和LinkedTransferQueue都是链表实现的阻塞队列,那么它们除了有界和无界外还有什么区别呢?
LinkedTransferQueue多了tranfer()和trytranfer()方法,这俩个方法主要作用在于,消费者生产了可以直接给生产者。它俩的区别在于tranfer()必须消费者消费了才可以返回值。trytranfer()无论消费者是否消费,立即返回。

SynchronousQueue不存储元素?
SynchprnousQueue不存储元素,生产一个元素,必须有消费者去消费。

LinkedBlockingDeque
LinkedBlockingDeque是个双向链表,可以用来实现工作密取,头尾都可以插入和删除,带有frist的方法表示头操作,带有last表示尾操作。如果不加 add默认addlast,remove默认为removefrist。

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

DelayQueue在企业开发中,常用于订单限时支付的场景,那么我们写一个小代码,来实现一下延时队列的功能

(1)实现Delayed.class

package Delayed;

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/**
 * 类:用来存放到队列的元素
 * @param <T>
 */
public class ItemVo<T> implements Delayed {
    private long activeTime;
    //队列传入的数据
    private T date;
    public ItemVo(long activeTime,T date){
        super();
        //因为Delayed内都是以纳秒为单位,所以需要进行转换
        //将用户传入的超时时间转为超时的时刻(纳秒)
        this.activeTime=TimeUnit.NANOSECONDS.convert
                (activeTime,TimeUnit.MILLISECONDS)+System.nanoTime();
        this.date=date;
    }
    //getDelay是Delayed接口中需要实现的方法
    //作用是返回离超时时刻还剩余的时间
    @Override
    public long getDelay(TimeUnit unit) {
        //让超时的时刻减去当前时刻
        long d=unit.convert(this.activeTime-System.nanoTime(),
                TimeUnit.NANOSECONDS);
        return d;
    }


    //compareTo比较方法,是Delayed接口继承CompareTo接口所要实现的
    //作用是按照剩余时间进行排序
    @Override
    public int compareTo(Delayed o) {
        long d=getDelay(TimeUnit.NANOSECONDS)-o.getDelay(TimeUnit.NANOSECONDS);
        return (d==0)?0:(d>0?1:-1);
    }

    public T getDate() {
        return date;
    }
}

(2)订单的实体类

package Delayed;

public class Order {
    //订单的编号
    private String orderNo;
    //订单的价格
    private Double orderMoney;
    public Order(String orderNo,Double orderMoney){
        super();
        this.orderNo=orderNo;
        this.orderMoney=orderMoney;
    }

    public String getOrderNo() {
        return orderNo;
    }

    public Double getOrderMoney() {
        return orderMoney;
    }
}

(3)存放订单信息的线程.class

package Delayed;

import java.util.concurrent.DelayQueue;

/**
 * 把订单加入队列的线程
 */
public class PutOrder implements Runnable {
    private DelayQueue<ItemVo<Order>> queue;

    public PutOrder(DelayQueue queue){
        this.queue=queue;
    }

    @Override
    public void run() {
         //床架订单类
         Order JDorder=new Order("No666",777.0);
         //创建Delay类,延时500ms,date为Order类
        ItemVo<Order> itemVo=new ItemVo<>(5000,JDorder);
        //将Delay类放入延时队列
        queue.offer(itemVo);
        System.out.println(JDorder.getOrderNo()+"订单  5000ms后会进入可见状态");
    }
}

(4)取出订单的线程.class

package Delayed;

import java.util.concurrent.DelayQueue;

public class GetOrder implements Runnable {
    private DelayQueue<ItemVo<Order>> queue;

    public GetOrder(DelayQueue queue){
        this.queue=queue;
    }
    @Override
    public void run() {
        try {
            ItemVo<Order> itemVo=queue.take();
            Order order=itemVo.getDate();
            System.out.println(order.getOrderNo() +"已被获取");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

(5)测试队列功能.class

package Delayed;

import java.util.concurrent.DelayQueue;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        //创建队列
        DelayQueue<ItemVo<Order>> queue=new DelayQueue<>();
        //让存队列开启
        new Thread(new PutOrder(queue)).start();
        //让取队列开启
        new Thread(new GetOrder(queue)).start();
        //为了更清晰的观察时间,让线程每隔500ms取输出一下
        for (int i=0;i<15;i++){
            Thread.sleep(500);
            System.out.println((i+1)*500+"ms");
        }
    }
}

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值