ArrayList 坑 的引发思考

单线程 ArrayList.remove()的坑

 public static void main(String[] args) {
        singleThread();
    }

    public  static void  singleThread(){
        ArrayList<String> list = new ArrayList<String>();
        list.add("刘一");
        list.add("刘二");
        list.add("单点");
        list.add("等待");
        list.add("饿饿");
        Iterator iter = list.iterator();
        while(iter.hasNext()){
            String str = (String) iter.next();
            if(str.equals("单点")){
                list.remove(str);
            }
        }

        System.out.println(list.size());
    }

上面这段问题代码引发的思考 ,运行报下面异常

org.xxy.rpc.controller.ArrayListDemo
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at org.xxy.rpc.controller.ArrayListDemo.singleThread(ArrayListDemo.java:21)
	at org.xxy.rpc.controller.ArrayListDemo.main(ArrayListDemo.java:9)

Process finished with exit code 1

通过错误提示;查看源码ArrayList.java:859 行;ArrayList 内部类Itr实现的迭代器 next()方法:

        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

next方法一上来 就调了checkForComodification 方法;
再 checkForComodification方法里 modCount != expectedModCount 就报异常;如下代码

      final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

问题找了到了modCount != expectedModCount导致;
这俩是SM东西??? 下面介绍他俩

下面是Arraylist.remove(object o)方法源码
  public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
没有发现什么问题,继续看 fastRemove(index)
fastRemove(int index)删除时,将 modCount++了 ;
expectedModCount值没有发现身影,
那岂不是 迭代器再next就报异常了;
问题好像清晰了;
再一看 删除操作是通过数组 copy 实现的,果然还是数组;
 数组 copy是 原生方法哦

    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }


结论:

1.modCount  是ArrayList 抽象父类AbstractList 所有,记录 结构上修改此列表的次数。
2.expectedModCount 是ArrayList 内部类 Itr implements Iterator 私有的;1.ArrayList 的 Iterator.next()方法会校验 expectedModCount == modCount,不一致就报 ConcurrentModificationException; 2.ArrayList.remove(object o)方法 会修改 ArrayList继承来的modCount;不会修改内部类 Itr里的 expectedModCount ;3.ArrayList 内部类 Itr遍历时。修改请使用迭代器自带的方法 如Iterator.remove();

在ArrayList 迭代器方法里;发现了内部类 Itr

public Iterator<E> iterator() {
    return new Itr();
}
     */
    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount; //俩个值一致

        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

 

Iterator.remove()方法源码,原因传参不为null ; 我们留意下下面else  里

也是先校验修改次数,这个在多线程里也是会报错的;下面会再写一文说明

在单线程里没有问题, 校验完成后 调用 ArrayList.remove(object o) ;删除里  在修改内部 expectedModCount = modCount;

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

多线程 ArrayList 内部 Iterator.remove() 的坑

上面说了 ArrayList 再多线程里Iterator.remove()也有问题 请看下面示例

直接运行也报异常 ConcurrentModificationException;想必原因大家都想到了;

Iterator.remove() 没有加锁,多线程并发时,导致 modCount值 脏读;不安全;

 private static void  multiThread(){
       final ArrayList<String> list = new ArrayList<String>();
        list.add("刘一");
        list.add("刘二");
        list.add("单点");
        list.add("等待");
        list.add("饿饿");

        new Thread(new Runnable() {
            public void run() {
                Iterator iter = list.iterator();
                while (iter.hasNext()) {
                    String str = (String) iter.next();
                    if (str.equals("单点")) {
                        iter.remove();
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                Iterator iter = list.iterator();
                while (iter.hasNext()) {
                    String str = (String) iter.next();
                    if (str.equals("刘二")) {
                        iter.remove();
                    }
                    System.out.println(str);
                }
                System.out.println(list.size());
            }
        }).start();
    }

错误

Exception in thread "Thread-1" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at org.xxy.rpc.controller.ArrayListDemo$2.run(ArrayListDemo.java:58)
	at java.lang.Thread.run(Thread.java:748)

问题来了 ArrayList 也没有多线程安全的呢???有 CopyOnWriteArrayList 这个是多线程安全的;

为什么CopyOnWriteArrayList 是安全的;我们分析下

CopyOnWriteArrayList 的源码学习

CopyOnWriteArrayList的最开始我是再数据驱动注册源码里看到的;
public class DriverManager {
    // 注册了JDBC驱动的集合
     private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new           
  CopyOnWriteArrayList<>();
......省略
}

CopyOnWriteArrayList 的多线程安全是通过 ReentrantLock 锁实现的;我们看下CopyOnWriteArrayList 的新增元素方法的实现add(E e) 

 public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

我们看到它再操作时;首先声明了一把 ReentrantLock 锁,再lock ,最后结束时 unlock;

ReentrantLock锁我们下面会单独介绍;

现在我们看看add 方法除了锁之外,其他的东西;首先获取当前数据对象 elements = getArray(); 

再 复制了一份数组对象 放在  新的数组 newElements里,新数组里进行了扩容+1 操作;也就是说新数组比旧数据长度多1;

最后一位就是多出来的,放了这个增加元素;

然后呢》》将新数组 赋给了CopyOnWriteArrayList的存储数组 array;

  /**
     * Sets the array.
     */
    final void setArray(Object[] a) {
        array = a;
    }

现在我们再看看CopyOnWriteArrayList的读源码

   /** The array, accessed only via getArray/setArray. */

private transient volatile Object[] array;

public E get(int index) {
        return get(getArray(), index);
    }

final Object[] getArray() {
        return array;
    }

private E get(Object[] a, int index) {
        return (E) a[index];
    }

它的读就简单的多了;没有什么花操作,直接返回数组值;也没有加锁;这里有个问题,就是写和读同时发生时,

因为写操作分好几个步骤(copt旧数据,增加新节点元素,修改旧数组指向)会有读到的是旧数组问题;

volatile 修饰 array 是为了禁止指令重排,和 内存可见性(工作内存与主存一致性);这里看看 JMM ,

特别说明 volatile 修饰下也  不是原则性的;

通过上面CopyOnWriteArrayList的源码解读我们发现他的特点:

1.写时复制机制

2.写操作加锁|解锁

3.读操作不加锁,数据

4.体现读写分离和最终一致性;

上面说了CopyOnWriteArrayList的多线程是通过ReenTranLock 重复锁实现的;下面我们来说下

锁;java里synchronized 和ReenTranLock俩类锁;他们有什么区别 值得思考;

Synchronized与ReentrantLock区别

Synchronized是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成

Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的,有什么区别,说白了就类似于操作系统来控制实现和用户自己敲代码实现的区别。前者的实现是比较难见到的,后者有直接的源码可供阅读。

很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。

锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized

Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术【内存值,旧值,期望值】。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:

        1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。通过lock.lockInterruptibly()来实现这个机制。

        2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。

公平锁、非公平锁的创建方式:

//创建一个非公平锁,默认是非公平锁
Lock lock = new ReentrantLock();
Lock lock = new ReentrantLock(false);
 
//创建一个公平锁,构造传参true
Lock lock = new ReentrantLock(true);
        3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

什么情况下使用ReenTrantLock:

答案是,如果你需要实现ReenTrantLock的三个独有功能时。

class MyThread implements Runnable {
 
	private Lock lock=new ReentrantLock();
	public void run() {
			lock.lock();
			try{
				for(int i=0;i<5;i++)
					System.out.println(Thread.currentThread().getName()+":"+i);
			}finally{
				lock.unlock();
			}
	}

Java虚拟机对synchronize的优化:

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级,关于重量级锁,前面我们已详细分析过,下面我们将介绍偏向锁和轻量级锁以及JVM的其他优化手段。

偏向锁

偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

轻量级锁

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

/**

 * Created by zejian on 2017/6/4.

 * Blog : http://blog.youkuaiyun.com/javazejian 

 * 消除StringBuffer同步锁

 */

public class StringBufferRemoveSync {

 

    public void add(String str1, String str2) {

        //StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用

        //因此sb属于不可能共享的资源,JVM会自动消除内部的锁

        StringBuffer sb = new StringBuffer();

        sb.append(str1).append(str2);

    }

 

    public static void main(String[] args) {

        StringBufferRemoveSync rmsync = new StringBufferRemoveSync();

        for (int i = 0; i < 10000000; i++) {

            rmsync.add("abc""123");

        }

    }

 

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值