单线程 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 |
|