程序猿学社的GitHub,欢迎Star
https://github.com/ITfqyd/cxyxs
本文已记录到github,形成对应专题。
在学习多线程的过程中,我们经常会听到ArrayList线程不安全。有个别社友就在说,我们在项目中用的好好的,也没有什么报错,实际上,大部分的人,在项目开发过程中,确实很少接触到这块。来我们一探究竟呗。看看为什么说ArrayList线程不安全。
一文了解ArrayList源码及扩容
在多线程环境下,操作同一资源,会有各种各样的问题出现,例如,在多线程环境下,操作ArrayList进行添加操作,就会出现java.util.ConcurrentModificationException的异常。我们常说某某是不是线程安全,实际上,是有一个前提的,是在多线程环境里,因为单线程过程中不会存在线程不安全的问题。
package com.cxyxs.thread.twelve;
import java.lang.reflect.Array;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* Description:转发请注明来源 程序猿学社 - https://ithub.blog.csdn.net/
* Author: 程序猿学社
* Date: 2020/2/20 20:00
* Modified By:
*/
public class Demo1 {
public static void main(String[] args){
List<Integer> lists = new ArrayList<>();
for (int i = 0; i < 50; i++) {
new Thread(()->{
for (int j = 0; j < 50; j++) {
lists.add(j);
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
}
while (true){
if (Thread.activeCount() == 2){
System.out.println(lists.size());
return;
}
}
//Thread.currentThread().getThreadGroup().list();
}
}
读过小学的,都知道50*50,是2500,为什么打印的结果是2463?
话不多说,上ArrayList的add源码
private int size;
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
size++就是线程不安全的,需要大家了解JMM的可见性,不了解可以大致看看我之前的文章。
【多线程并发编程】六 什么是线程安全?
package com.cxyxs.thread.twelve;
import java.lang.reflect.Array;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* Description:转发请注明来源 程序猿学社 - https://ithub.blog.csdn.net/
* Author: 程序猿学社
* Date: 2020/2/20 20:00
* Modified By:
*/
public class Demo1 {
public static void main(String[] args){
//线程不安全
//List lists = new ArrayList<>();
// 优化方案1
List<Integer> lists = new Vector<>();
for (int i = 0; i < 50; i++) {
new Thread(()->{
for (int j = 0; j < 50; j++) {
lists.add(j);
try {
Thread.sleep(1);
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
}
while (true){
if (Thread.activeCount() == 2){
System.out.println(lists.size());
return;
}
}
//Thread.currentThread().getThreadGroup().list();
}
}
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
之前在开发过程中,经常发现有ConcurrentModificationException这个报错。
看名字其义,意思就是多线程修改异常,也就说多个线程对同一份资源进行写操作会有这种异常报错。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
我们看一看哪里调用了这个方法
public E next() {
synchronized (Vector.this) {
checkForComodification();
int i = cursor;
if (i >= elementCount)
throw new NoSuchElementException();
cursor = i + 1;
return elementData(lastRet = i);
}
}
public synchronized Iterator<E> iterator() {
return new Itr();
}
疑问
本人在调用add时,发现modCount每次调用add就会+1,但是expectedModCount一直没有变,就有点困惑。个人感觉应该跟这些类的add方法应该有关系。对这方面了解很深的大佬,可以在下方留言。
//优化方案二:
List<Integer> lists =Collections.synchronizedList(new ArrayList<>());
package com.cxyxs.thread.twelve;
import java.lang.reflect.Array;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
/**
* Description:转发请注明来源 程序猿学社 - https://ithub.blog.csdn.net/
* Author: 程序猿学社
* Date: 2020/2/20 20:00
* Modified By:
*/
public class Demo1 {
public static void main(String[] args){
//线程不安全
//List lists = new ArrayList<>();
// 优化方案1
//List lists = new Vector<>();
//优化方案二:
//List lists =Collections.synchronizedList(new ArrayList<>());
//优化方案三
List<Integer> lists = new CopyOnWriteArrayList();
for (int i = 0; i < 50; i++) {
new Thread(()->{
for (int j = 0; j < 50; j++) {
lists.add(j);
try {
Thread.sleep(1);
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
}
while (true){
if (Thread.activeCount() == 2){
System.out.println(lists.size());
return;
}
}
//Thread.currentThread().getThreadGroup().list();
}
}
通过多次测试,发现结果为2500.
写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。优点是如果调用者没有修改该资源,就不会有副本(private copy)被建立,因此多个调用者只是读取操作时可以共享同一份资源。
我们通过源码来看一看,jdk大佬是如何实现CopyOnWriteArrayList的add方法的。
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();
}
}
public ListIterator<E> listIterator() {
return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the array */
private final Object[] snapshot;
/** Index of element to be returned by subsequent call to next. */
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
}