ArrayList

ArrayList 是 java 集合框架中常用的数据结构,实现了List接口,同时还实现了 RandomAccess、Cloneable、Serializable 接口!

System.arraycopy() 方法

源码:

    // 我们发现 arraycopy 是一个 native 方法,接下来我们解释一下各个参数的具体意义
    /**
    *   复制数组
    * @param src 源数组
    * @param srcPos 源数组中的起始位置
    * @param dest 目标数组
    * @param destPos 目标数组中的起始位置
    * @param length 要复制的数组元素的数量
    */
    public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

Arrays.copyOf()方法

源码:

    public static int[] copyOf(int[] original, int newLength) {
    	// 申请一个新的数组
        int[] copy = new int[newLength];
	// 调用System.arraycopy,将源数组中的数据进行拷贝,并返回新的数组
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

ArrayList是如何扩容的?

jdk 1.7之前默认容量是10

//默认初始化容量
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
 public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

//集合的长度
private int size;

//集合存元素的数组
Object[] elementData; 

//默认的容量
private static final int DEFAULT_CAPACITY = 10;

private static int calculateCapacity(Object[] elementData, int minCapacity) {
     	//判断集合存储元素的数组是否为空
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //如果当前最小容量小于默认容量10 就将默认容量10返回
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

 private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
     	//判断最小容量是否大于当前数组实际容量(就是判断是否要扩容)
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

//扩容的核心方法
private void grow(int minCapacity) {
    	//原来容量
        int oldCapacity = elementData.length;
    	//扩容1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            //判断扩容之后还是小了,就直接把最小容量当做新的容量
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
    	//拷贝数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

对于ArrayList ,new无参对象时,底层是一个空数组,当添加第一个元素时,会进行扩容,将底层数组长度扩为10,
其中扩容触发的条件是:存元素时,即先让size+1的值(也就是最小容量的值)判断是否大于底层elementData.length的长度,如果大于,则先扩容再添加,扩容的倍数为1.5倍。

当我们初始化一个arraylist数组时,jdk1.8 默认是为空的,当我调用add方法时,会进行第一次的扩容,将数组长度扩容为10;当我们数组的元素大于数组的长度10时,会触发自动扩容机制,首先创建一个新的数组,这个数组的长度是原数组的1.5倍,然后使用Arrays.copyOf方法把原数组的数据拷贝到新数组里面

ArrayList频繁扩容导致添加性能急剧下降,如何处理?

当需要加入的数据量特别多,如果是无参的话,数组的容量是逐渐增加的,那么就会触发很多次的扩容,因为扩容的时候会使用到数组拷贝,这个过程很耗费性能,会导致ArrayList效率下降

可以在创建集合时指定初始容量大小,减少扩容次数

Arraylist 与 LinkedList 区别?

  1. 是否保证线程安全: ArrayListLinkedList 都是不同步的,也就是不保证线程安全;
  2. 底层数据结构: Arraylist 底层使用的是 Object 数组LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)
  3. 插入和删除是否受元素位置的影响:ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② LinkedList 采用链表存储,所以对于add(E e)方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i插入和删除元素的话((add(int index, E element)) 时间复杂度近似为o(n))因为需要先移动到指定位置再插入。
  4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。
  5. 内存空间占用: ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。

Array和ArrayList的区别?

  • array是数组,声明好后,长度是固定的;ArrayList是集合,底层是动态数组,长度可以改变
  • array可以存储基本类型和对象类型;ArrayList只能存储对象类型

ArrayList插入或删除元素一定比LinkedList慢吗?

不一定,得看操作元素的位置

ArrayList在插入或删除元素时都会拷贝数组,增删越靠前的元素,拷贝的元素越多,效率越低;

public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index;//把索引位置后的元素后移一格
    elementData[index] = element;
    size++;
}

LinkedList在插入或删除元素时,调用Node方法,折半查找要操作的元素,如果数据量大,并且操作的元素在中间,这时候效率也会很慢

//找元素的方法 
Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) {
            //从头往后找
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            //从尾往后找
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

ArrayList线程不安全

因为ArrayList是动态数组,size(数组的长度)是成员变量,是共享的;当两个线程同时操作add方法时,线程a添加一个元素,数组下标增加,此时线程b在操作时发现没有这个下标,就会抛出数组越界异常;或者在赋值操作时候,多个线程操作同一个下标,会出现值被覆盖,值为null的现象;

解决:

  • 使用Vector类,因为该类的方法加了同步锁(synchronized)

  • 使用Collections.synchronizedCollection(Collection c):

    Object mutex = new Object()。对此对象使用synchronized

  • 使用CopyOnWriteArrayList,COW采用自旋锁(jdk1.6升级为synchronize)对写加锁,读不加锁,数组变量有使用volatile保证可见性,增删创建一新数组,读取的是原数据,适合读多写少场景。

Write的时候总是要Copy(将原来array复制到新的array,修改后,将引用指向新数组)。任何可变的操作(add、set、remove等)都通过ReentrantLock 控制并发。

复制ArrayList的5种方法

  1. 使用ArrayList的构造方法,底层实际上调用了Arrays.copyOf方法来对数组进行拷贝。这个拷贝调用了系统的native arraycopy方法,注意这里的拷贝是引用拷贝,而不是值的拷贝。
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
  1. 使用addAll方法
 public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }
  1. 使用Collections.copy(dest,src):首先要指定要复制数组的大小(要大于或等于被复制数组的大小),因为该方法首先会判断两个数组的长度大小;
  2. 使用stream流的方式
  3. 直接使用clone方法
ArrayList<Integer> list = new ArrayList<Integer>();
        list.add(1);
        list.add(2);
        list.add(3);

        //构造方法
        ArrayList<Integer> listCopy1 = new ArrayList<Integer>(list);
        System.out.println(listCopy1);

        //addAll方法
        ArrayList<Integer> listCopy2 = new ArrayList<Integer>();
        listCopy2.addAll(list);
        System.out.println(listCopy2);

        //Collections.copy方法
        ArrayList<Integer> listCopy3 = new ArrayList<Integer>(
                Arrays.asList(new Integer[list.size()]));
        Collections.copy(listCopy3, list);
        System.out.println(listCopy3);

        //stream流方法
        List<Integer> listCopy4 = list.stream().collect(Collectors.toList());
        System.out.println(listCopy4);

 		//使用clone方法
        ArrayList<Integer> listCopy5 = (ArrayList<Integer>) list.clone();
        System.out.println(listCopy5);

HashMap

HashMap主要用来存放键值对,可以存放null的key和value,但是为null的key只能有一个,key是唯一的,存储的元素是无序的,线程不安全

HashMap底层实现

JDK1.8之前,是由数组加链表组成

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过(n-1) & hash公式(n为数组长度)得到key在数组中存放的下标,如果当前下标位置存在元素(哈希冲突),就要判断当前元素与要存入元素的hash值和key是否相同,如果相同直接覆盖;如果不同,就通过拉链法解决冲突。

所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

我们知道,链表查找数据必须从第一个元素开始查找,直到找到为止,时间复杂度为O(n),所以当链表越来越长是,hashmap的效率越来越低;

那么怎么解决这个问题?

  • JDK1.8开始采用:数组+链表+红黑树 结构来实现HashMap,当链表的长度(元素)大于阈值8(在链表转为红黑树之前会判断,如果当前数组的长度小于64,会先进行数组扩容,而不是转为红黑树)时,就会将链表转为红黑树,以提高查找效率;

类的属性:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    // 序列号
    private static final long serialVersionUID = 362498820763181265L;
    // 默认的初始容量是16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    // 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    // 默认的填充因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 当桶(bucket)上的结点数大于这个值时会转成红黑树
    static final int TREEIFY_THRESHOLD = 8;
    // 当桶(bucket)上的结点数小于这个值时树转链表
    static final int UNTREEIFY_THRESHOLD = 6;
    // 桶中结构转化为红黑树对应的table的最小容量
    static final int MIN_TREEIFY_CAPACITY = 64;
    // 存储元素的数组,总是2的幂次倍
    transient Node<k,v>[] table;
    // 存放具体元素的集
    transient Set<map.entry<k,v>> entrySet;
    // 存放元素的个数,注意这个不等于数组的长度。
    transient int size;
    // 每次扩容和更改map结构的计数器
    transient int modCount;
    // 临界值(容量*填充因子) 当实际大小超过临界值时,会进行扩容
    int threshold;
    // 加载因子
    final float loadFactor;
}
  • loadFactor 加载因子

    loadFactor 加载因子是控制数组存放数据的疏密程度,loadFactor 越趋近于 1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor 越小,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。

    loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值

    给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。

为什么HashMap要用数组加链表来实现?

HashMap的key经过扰动函数之后得到一个hash值,然后通过公式(数组的长度-1)& hash值得到key在数组中的下标,如果当前下标位置存在元素,说明存在哈希冲突,就要判断当前元素与要存入元素的hash值和key值是否相同,如果相同就直接覆盖,如果不同,就要通过链表来解决哈希冲突;这是jdk1.7的做法,如果链表的长度过长,会导致查找和插入效率变低。所有jdk1.8新增加了红黑树

HashMapd的put方法的大致流程

  1. 首先根据key的值计算出hash值,然后通过(n-1)&hash操作找到该元素在数组中存储的下标
  2. 如果当前数组为空,则会调用resize方法进行初始化,得到一个默认容量16,阈值为12的数组
  3. 如果没有哈希冲突,就直接放入该数组的下标
  4. 如果有哈希冲突,并且key相同,就直接覆盖value的值
  5. 如果冲突后,发现该节点是红黑树(TreeNode),就把该节点挂在树上
  6. 如果冲突后是链表,判断链表的长度是否大于阈值8,如果大于8并且数组的容量小于64,就会进行扩容;

如果链表长度大于8并且数组容量大于64,就会把链表转为红黑树;否则,就会插入到链表中,如果有相同的key,就会直接覆盖。

jdk1.8对HashMap做了哪些优化?

  1. 引入了红黑树,当链表长度大于8并且数组长度大于64时,链表转为红黑树,解决了链表过长,导致查询效率降低的问题,链表查询时间复杂度O(n),红黑树增删查时间复杂度都为O(logn)
  2. 哈希冲突时,头插改尾插,可以避免扩容后相对位置的倒序,避免并发环境下,扩容产生循环链表,导致死循环
  3. 2倍扩容后,在计算数组下标时,直接判断hash高位值(前16位),如果为0,则原位置保持不变,如果为1,原位置加上原数组长度,这样省去了重新计算hash的时间,这样算出的数组下标具有随机性,减少哈希冲突

HashMap的扩容方式

jdk1.7中,扩容会重新计算hash值,并且会遍历hash表所有元素,是非常耗时的;

HashMap在容量大于阈值(加载因子*当前数组容量 =0.75*16=12 )时,就会调用resize方法进行扩容,将hashmap的大小扩容为原来的2倍,并将原来的对象放入新数组当中;

当我们初始化一个Hashmap集合时,默认集合容量大小为0,当我们调用put方法时,hashmap会进行扩容,将大小扩容为默认的容量16,阈值为12;当向集合添加元素时,如果元素的个数大于阈值12时,就会进行扩容,将大小扩容为原来的2倍(左移一位),然后遍历数组,判断要转移的元素是单个元素、链表或者红黑树;

  • 如果是单个元素,就直接放入到新数组

    if (e.next == null)
        newTab[e.hash & (newCap - 1)] = e;
    
  • 如果是链表,会有一个高低位的选择,计算的下标位置不变或者计算的(下标位置+原数组的长度)

Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
    next = e.next;
    if ((e.hash & oldCap) == 0) {
        if (loTail == null)
            loHead = e;
        else
            loTail.next = e;
        loTail = e;
    }
    else {
        if (hiTail == null)
            hiHead = e;
        else
            hiTail.next = e;
        hiTail = e;
    }
} while ((e = next) != null);
  • 如果是红黑树,遍历双向链表统计哪些元素在扩容完是原位置还是新位置,这样遍历完双向链表后,就会得到两个子链表,一个放在原下标位置,一个放在新下标位置,如果原下标位置或新下标位置没有元素,则红黑树不用拆分,否则判断这两个子链表的长度,如果超过八,则转成红黑树放到对应的
    位置,否则把单向链表放到对应的位置。
  • 元素转移完了之后,在把新数组对象赋值给HashMap的table属性,老数组会被回收。

HashMap 的长度为什么是2的幂次方

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。

为什么HashMap是不安全的?

jdk1.7,头插法——>当并发执行扩容操作时会造成环形链和数据丢失的情况。

HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。

jdk1.8,尾插法——>在并发执行put操作时会发生数据覆盖的情况。

向HashMap集合中添加元素会存在覆盖的现象,导致了线程不安全。

synchronized关键字

synchronized特性

synchronized是一种对象锁(锁的是对象而非引用),作用粒度是对象,java中每个对象都可以上锁(同一时间只有一个线程能上锁成功),而且通过对象内部存储的markword标记锁状态。

特性:

  • 原子性:保证操作不可分割(被Synchronized修饰的对象、方法和代码块,在执行操作之前都必须获得类或对象的锁,直到执行完毕才能释放锁,这个期间是无法被打断的),要么所有的操作都执行,要么都不执行
  • 可见性:jmm中主内存和每个线程的工作内存的数据
  • 有序性:程序是顺序执行的
  • 可重入性:可以重复使用的锁(可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁)

synchronized底层实现

在jvm中,java对象由三部分组成:

  • 对象头:主要是MarkWord(存储对象的hashcode(对象的引用地址)、锁信息或GC分代年龄或锁状态标志),类型指针(该对象是哪个类的实例,数组长度(数组才有))

  • 实例数据:真实记录一个对象包含的数据,比如说一个person对象,里面可能包含年龄、性别、身高等等

    其中数据为字符串的,要引用到字符串常量池。

  • 对齐填充:填充部分仅起到占位符的作用, 原因是HotSpot要求对象起始地址必须是8字节的整数,假如不是,就采用对齐填充的方式将其补齐8字节整数倍,那么为什么是8呢?原因是64位机器能被8整除的效率是最高的。

在这里插入图片描述

是由一对monitorenter和monitorexit指令来实现同步的,每一个锁对象都会管理一个monitor(监视器,它才是真正的锁对象),当计数器count为0时释放锁

synchronized锁优化

synchronized锁升级

所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。

锁有四种状态,并且根据实际情况进行锁的升级:无锁——>偏向锁——>轻量级锁——>重量级锁

  • 偏向锁:减少线程获取锁的代价(它的本质就是让锁来记住请求的线程),如果一个线程获取了锁,那么锁就进入了偏向模式,此时Mark Word的结构也就变为偏向锁结构。此后获取锁不再需要额外的操作,只需要判断对象头里面的锁标志位和线程id即可,如果是当前线程,直接进入同步操作,减少开销;
  • 轻量级锁:由偏向锁升级而来,当第二个线程申请(只是申请,并没有竞争)同一个锁对象时,偏向锁就会升级为轻量级锁。
  • 重量级锁:由轻量级锁升级,当多个线程同时竞争锁时,就会升级成重量级锁,此时性能开销会变大(追求吞吐量-高并发)

synchronized锁消除和锁粗化

锁消除:jvm在即时编译时,对上下文进行扫描,去除不可能存在竞争的锁。比如锁的对象是私有变量,就不存在竞争关系

锁粗化:扩大锁的范围,避免反复加锁和释放锁

for(int i=0;i<1000;i++){
    synchronized (Test.class){
        System.out.print("hello");
    }
}

//锁粗化
synchronizde(Test.class){
    for(int i=0;i<1000;i++){
        System.out.print("hello");    
	}
}

synchronized自旋锁和自适应自旋锁

自旋锁:当其它线程获取轻量级锁失败时,不会挂起,而是会进行自旋锁的优化手段,即不断的尝试去获取锁,在一定的时间内(或者在一定的自旋的次数内),如果获取到锁,就顺利执行;如果到时还没获取到锁,就会挂起;自旋锁适用于锁占有者占有锁的时间比较短的情况;

缺点:如果锁一直被其它线程长时间占有,会带来许多的性能开销

自适应自旋锁:对自旋锁的进行优化,它的自旋次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的占有者的状态来决定;

synchronized和lock区别

​ 1、synchronized是java的关键字; Lock是一个类

​ 2、synchronized无法判断获取锁的状态; Lock可以判断是否获取到了锁

​ 3、synchronized 会自动释放锁; Lock 必须手动释放锁(否则会发生死锁)

​ 4、synchronized 线程1(获得锁,阻塞) 线程2(傻傻的等); Lock锁就不一定会等,会尝试获得锁

​ 5、synchronized 可重入锁,不可中断,非公平锁; Lock,可重入锁,可中断可不中断,可以设置公平/非公平

​ 6、synchronized 适合少量的代码同步问题; Lock适合大量的代码同步问题

​ 7、synchronized可以锁方法或代码块,lock只能锁代码块

volatile关键字

  1. 保证了多个线程对某个共享变量进行操作时的可见性
  2. 禁止指令重排,保证有序性

指令重排序:为了优化程序性能,对原有指令的执行顺序进行优化重新排序。(重排序发生的阶段:编译重排序,cpu重排序)

volatile如何实现禁止指令重排?

内存屏障(memory barrier):相当于一个同步点,在执行这个同步点指令(lock addl)之前的所有读写操作都执行完毕之后才可以执行这个同步点指令之后的操作。

内存屏障的作用:是一种cpu指令,阻止屏障两侧的指令重排序,内存屏障可以让cpu或编译器在内存访问上有序。

内存屏障的插入策略:

  • 在每个volatile写操作前加插入一个StoreStore屏障,在volatile写之后插入一个StoreLoad屏障
  • 在每个volatile读操作后插入一个LoadLoad屏障——>LoadStore屏障

volatile重排序的规则:

  • 如果第一个操作是volatile读,那么无论第二个操作是什么,都不能重排序
  • 如果第二个操作是volatile写,无论第一个操作是什么,都不能重排序
  • 如果第一个操作是volatile写,第二个操作是volatile读,不能重排序

volatile使用场景:

  • 修饰标记变量:volatile boolean flag=false;
  • 单例模式中的双重检查

CAS详解

比较并交换,CAS是一条CPU的原子指令,它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,操作过程是原子的;

// setup to use Unsafe.compareAndSwapInt for updates  
private static final Unsafe unsafe = Unsafe.getUnsafe();  
private static final long valueOffset;// 注意是静态的  
private volatile int value;
static {  
  try {  
    valueOffset = unsafe.objectFieldOffset  
        (AtomicInteger.class.getDeclaredField("value"));// 反射出value属性,获取其在内存中的位置  
  } catch (Exception ex) { throw new Error(ex); }  
}  
  //重点呀!!!
public final boolean compareAndSet(int expect, int update) {  
  return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
}  

底层原理

Unsafe是CAS的核心类,Unsafe类的所有方法都是native的,由于java无法访问底层系统,需要通过本地方法(native)访问,Unsafe类可以直接操作特定内存的数据

valueOffset:表示变量的值在内存中的偏移地址

CAS的应用

Java中java.util.concurrent.atomic并发包中的数据进行处理就是利用的CAS原理,以AtomicInteger为例

public final int getAndSet(int newValue) {
    return unsafe.getAndSetInt(this, valueOffset, newValue);
}

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

public final int getAndDecrement() {
    return unsafe.getAndAddInt(this, valueOffset, -1);
}

CAS缺点

  1. ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

解决:ABA问题的解决思路就是使用版本号,类似于数据库的版本控制,。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

atomic包里提供了一个类AtomicStampedReference来解决ABA问题,这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值

  1. 循环时间长开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

CAS在执行自增自减的时候,如果不成功,会一直循环,直道成功为止

  1. 只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。

从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

原子类

img

AtomicInteger详解

AtomicInteger类中的操作的思想基本上是基于CAS+volatile。保证在多线程高并发情况下的i++的操作,

//使用unsafe的CAS来更新
private static final Unsafe unsafe = Unsafe.getUnsafe();
//偏移量
private static final long valueOffset;
static {
    try {
        //初始化value的起始地址的偏移量
        valueOffset = unsafe.objectFieldOffset
 (java.util.concurrent.atomic.AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) {
        throw new Error(ex);
    }
}

//值 - 使用volatile修饰
private volatile int value;

原子方法:

//直接操作内存 设置新值
public final void lazySet(int newValue) {
    unsafe.putOrderedInt(this, valueOffset, newValue);
}

//直接操作内存 设置新值并且返回旧值
public final int getAndSet(int newValue) {
    return unsafe.getAndSetInt(this, valueOffset, newValue);
}
// 获取当前的值,并自增,相当于线程安全版本的i++操作
public final int getAndIncrement()
// 获取当前的值,并自减,相当于线程安全版本的i--操作
public final int getAndDecrement() 
// 获取当前的值,并自增,相当于线程安全版本的++i操作
public final int incrementAndGet() 
// 如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
boolean compareAndSet(int expect, int update) 

1、基本类型
AtomicInteger、AtomicBoolean、AtomicLong

getAndIncrement() // 原子化 i++
getAndDecrement() // 原子化的 i--
incrementAndGet() // 原子化的 ++i
decrementAndGet() // 原子化的 --i
// 当前值 +=delta,返回 += 前的值
getAndAdd(delta) 
// 当前值 +=delta,返回 += 后的值
addAndGet(delta)
//CAS 操作,返回是否成功
compareAndSet(expect, update)
// 以下四个方法
// 新值可以通过传入 func 函数来计算
getAndUpdate(func)
updateAndGet(func)
getAndAccumulate(x,func)
accumulateAndGet(x,func)
版权声明:本文为XIAOC521原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/XIAOC521/p/16545260.html