原因解释
首先说一下什么是线程不安全:线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
如图,List接口下面有两个实现,一个是ArrayList,另外一个是vector。 从源码的角度来看,因为Vector的方法前加了,synchronized 关键字,也就是同步的意思,
sun公司希望Vector是线程安全的,而希望arraylist是高效的.
ArrayList线程不安全解释
首先我们来看一下ArrayList添加元素时的源码
/** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return <tt>true</tt> (as specified by {@link Collection#add}) */
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
我们注意到elementData[size++] = e;
这条语句其实就是添加元素的关键,其实这条语句可以拆成两条语句来执行也就是elementData[size] = e
和 size++
,试想一下,如果在多线程访问同一个list的时候,线程A执行到elementData[size] = e
时,时间片到了让出CPU,此时线程B执行,当线程B执行到elementData[size] = e
此时size
还没有加一,(假设此时size=10)也就是说线程A在list[10]
位置上添加的元素被线程B添加的元素覆盖了,然后A,B两个线程都执行size++
;这样造成的影响就是size
变成12
了,但是在第11
个位置上却没有元素也就是说list[11]=null
我嗯通过下面一段代码来验证下:
package com.github.thread;
import java.util.List;
/** *@DESCRIPTION 线程类,用来操作list *@AUTHOR SongHongWei *@TIME 2018/8/30-9:05 *@PACKAGE_NAME com.github.thread **/
public class ListTask implements Runnable {
private List<String> list;
ListTask(List list)
{
this.list = list;
}
@Override
public void run()
{
try {
Thread.sleep(10);//线程睡眠10ms
}catch (InterruptedException e){
e.printStackTrace();
}
//把当前线程名称加入list中
list.add(Thread.currentThread().getName());
}
public List<String> getList()
{
return list;
}
public void setList(List<String> list)
{
this.list = list;
}
}
测试类
package com.github.thread;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
/** *@DESCRIPTION 多线程, 线程池, 队列 *@AUTHOR SongHongWei *@TIME 2018/8/22-17:08 *@PACKAGE_NAME prodmng.songhw.thread **/
public class Client {
public static void main(String[] args)
throws Exception
{
//List<String> list = new Vector(); //线程安全
List<String> list = new ArrayList<>();//线程不安全
ListTask listThread = new ListTask(list);//实例化线程类
for (int i = 0; i < 100; i++)
{
Thread thread = new Thread(listThread, String.valueOf(i));//循环新开线程
thread.start();
}
//等待子线程执行完
Thread.sleep(2000);
System.out.println("list的总长度为"+listThread.getList().size());
//输出list中的值
for (int i = 0; i < listThread.getList().size(); i++)
{
System.out.print(listThread.getList().get(i) + " ");
}
}
}
执行结果为:
list的总长度为98
null null 0 3 1 5 4 13 14 10 15 11 8 7 9 16 12 27 21 28 20 26 25 23 19 17 24 22 18 39 36 33 38 34 35 30 32 31 37 29 49 46 42 43 41 44 60 47 51 52 58 59 54 55 56 45 53 50 57 48 40 69 65 68 70 62 61 71 63 66 64 74 77 80 72 76 82 78 79 73 75 81 83 89 86 84 88 90 87 94 92 91 85 93 99 95 96 98
Process finished with exit code 0
根据执行结果我们看出,实际上我期待的list结果应该为100
,而不是98
,并且list里面还出现了null
元素
其实不止会出现上述问题,多执行几次我们会发现,程序居然还会抛出异常,下面给出异常错误信息:
Exception in thread "69" java.lang.ArrayIndexOutOfBoundsException: 73
at java.util.ArrayList.add(ArrayList.java:441)
at com.github.thread.ListTask.run(ListTask.java:49)
at java.lang.Thread.run(Thread.java:744)
list的总长度为98
0 1 2 18 10 8 5 17 6 14 11 21 13 19 20 4 12 15 22 27 30 26 28 25 29 24 7 16 23 34 33 35 36 31 32 40 42 39 43 38 37 41 46 45 47 48 54 50 51 44 52 59 53 66 58 60 62 61 67 65 57 49 64 55 56 63 81 73 79 74 78 80 70
null 75 76 77 71 68 72 82 90 88 84 87 91 86 83 89 85 95 99 92 96 94 97 93 98
从上面结果可以看出,程序抛出了一个ArrayIndexOutOfBoundsException
异常以及list
中出现了null
元素两个问题,null
元素出现的原因已经解释过了,现在我们来分析ArrayIndexOutOfBoundsException
异常出现的原因
直观上理解ArrayIndexOutOfBoundsException
应该是数组下标越界,我们再来看一下ArrayList
新增元素的源码
/** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return <tt>true</tt> (as specified by {@link Collection#add}) */
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
之前我们关注的是elementData[size++] = e
语句,现在我们来看看ensureCapacityInternal(size + 1);
这条语句是干嘛的,其实ensureCapacityInternal
这个方法主要是对List
的扩容,顺便提一下,List
的默认容量是10,每次扩容大小为原来的1.5倍,源码的扩容方式是通过以为来运算,这样的运算效率更高(int newCapacity = oldCapacity + (oldCapacity >> 1);
)好了,言归正传,知道了ensureCapacityInternal
是扩容list
后我们来模拟一个场景,假设A线程执行到ensureCapacityInternal
时,此时的size=9
,还未到达扩容的条件,然后线程A让出CPU,线程B又进来执行,当线程B执行到ensureCapacityInternal(size+1)
时,由于线程A还没有进行添加元素的操作,所以此时size=9
,所以线程B此时也没有对List
进行扩容,接着线程B继续执行,添加元素,size+1
,这时候size=10
,就在这时B线程执行结束,A线程继续执行,由于刚才A线程已经判断过不需要扩容,所以直接添加元素,但问题是B线程执行后size=10
了,A线程再往里添加元素自然就报数组下标越界了.
Vector 线程安全
Vector 源码里对add操作加了同步锁,所以不会造成线程安全问题,但是由于加了同步锁,所以执行效率上就成了问题
Vector新增元素的源码
/** * Adds the specified component to the end of this vector, * increasing its size by one. The capacity of this vector is * increased if its size becomes greater than its capacity. * * <p>This method is identical in functionality to the * {@link #add(Object) add(E)} * method (which is part of the {@link List} interface). * * @param obj the component to be added */
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
运行结果
list的总长度为100
2 0 1 8 11 7 10 13 9 6 5 4 3 21 14 17 19 18 12 15 23 16 24 22 20 36 32 31 34 37 26 30 29 35 25 33 27 28 53 41 51 52 46 49 47 42 61 40 59 38 60 43 39 62 44 54 67 48 45 50 56 64 58 63 66 72 83 55 76 80 75 79 78 68 71 70 73 82 74 81 77 69 57 65 97 90 92 95 88 85 94 91 87 86 96 99 98 89 93 84
Process finished with exit code 0
其他线程安全的方法
由于Vector已经不建议使用了,所以我们如果想使用线程安全的List时可以使用
List<String> list1 = Collections.synchronizedList(new ArrayList<String>());
这种方式去实现
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/28984.html