ArrayList为什么是线程不安全
Arraylist是通过一个Object的数组elementData来实现的,而size是用来保存当前数组已经添加了多少元素。
add方法
add方法主要是三个步骤
1、当对元素进行添加是,通过ensureCapacityInternal方法来判断容器(数组)的容量是否足够,size+1如果超过了数组的容量,则进行扩容操作
2、将元素添加到数组中
3、size++
所以这里如果是多线程的环境下,便会产生问题
1.空值和值覆盖的问题(这里假设两条线程A线程,B线程)
【1】size为0,也就是数组一开始是0
【2】线程A开始添元素,检测到size+1为1,不需要进行扩容,进行元素添加elementData[0]=A,那么就将A放在索引为1的位置上,此时B线程进来,检测到size还是等于0,不需要扩容,那么也将值B放在elementData下标为0的位置上
【3】此时A线程执行size++,将size变成1,B线程执行size++,将size变成2
这样在AB线程执行完之后elementData[0]=B(A线程的值被覆盖),但是elementData[1]=null,size = 2,那么下次再次进入add方法的时候size的值就是2
2.数组下标越界(ArrayIndexOutOfBoundsException)
【1】列表的大小为9,也就是size= 9
【2】线程A 进入add方法,所以获得的size值为9,进入ensureCapacityInternal方法发现并不需要扩容
【3】线程B此时也进入add方法,它获得的size也是9,进入ensureCapacityInternal方法发现也并不需要扩容
【4】线程A开始进行设置值操作, elementData[9] = e 操作,并且size++。此时size变为10。
【5】线程B也开始进行设置值操作,它尝试设置elementData[10] = e,而elementData没有进行过扩容,它的下标最大为9。于是此时会报出一个数组越界的异常ArrayIndexOutOfBoundsException.
package com.qjx.unsafe;
import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
public class ArrayListTest {
static List<Integer> list = new ArrayList<>(10);
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
protoTest();
}, "A").start();
new Thread(() -> {
protoTest();
}, "B").start();
Thread.sleep(1 * 1000);
System.out.println("数组的总长度为:" + list.size());
}
public static void signalTest() {
List<String> list = Arrays.asList("a", "b", "c", "d");
list.forEach(System.out::println);
}
public static void protoTest() {
for (int i = 0; i < 1000000; i++) {
list.add(i);
}
}
}
解决集合类不安全的三种方法
1.使用Vector
package com.qjx.unsafe;
import java.sql.Time;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.TimeUnit;
/*
解决集合不安全的方法
*/
public class SolveList {
static List<Integer> list = new Vector<>();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
test01();
},"A").start();
new Thread(()->{
test01();
},"B").start();
TimeUnit.SECONDS.sleep(1);
System.out.println(list.size());
}
//第一种方法使用
public static void test01(){
for (int i = 0; i < 1000; i++) {
list.add(i);
}
}
}
2.使用Collections.synchronizedList()
package com.qjx.unsafe;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
/*
解决集合类不安全的第二种方法
*/
public class SolveListCollections {
static List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
test01();
},"A").start();
new Thread(()->{
test01();
},"B").start();
TimeUnit.SECONDS.sleep(1);
System.out.println(list.size());
}
public static void test01(){
for (int i = 0; i <1000 ; i++) {
list.add(i);
}
}
}
3.使用CopyOnWriteArrayList
package com.qjx.unsafe;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
public class SolveListCopyOnWriteList {
static List<Integer> list = new CopyOnWriteArrayList<Integer>();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
test01();
},"A").start();
new Thread(()->{
test01();
},"B").start();
TimeUnit.SECONDS.sleep(1);
System.out.println(list.size());
}
public static void test01(){
for (int i = 0; i <1000 ; i++) {
list.add(i);
}
}
}
写入时复制(CopyOnWrite)思想
写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种优化策略。其核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。读写分离,写时复制出一个新的数组,完成插入、修改或者移除操作后将新数组赋值给array
CopyOnWriteArrayList为什么并发安全且性能比Vector好
我知道Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况
线程不安全的类HashSet
package com.qjx.unsafe;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
public class UnSafeSet {
static Set<String> set =new HashSet<>();
public static void main(String[] args) {
for (int i = 0; i <30 ; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
解决的方法
1.Collections.synchronizedSet(new HashSet<>())
package com.qjx.unsafe;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
public class UnSafeSet {
//解决set不安全的第一种方法
static Set<String> set = Collections.synchronizedSet(new HashSet<>());
//static Set<String> set =new HashSet<>();
public static void main(String[] args) {
for (int i = 0; i <30 ; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
2.使用CopyOnWriteArraySet<>()
package com.qjx.unsafe;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
public class UnSafeSet {
//解决set不安全的第一种方法
//static Set<String> set = Collections.synchronizedSet(new HashSet<>());
//static Set<String> set =new HashSet<>();
//解决set不安全的第二种方法
private static Set<String> set = new CopyOnWriteArraySet<>();
public static void main(String[] args) {
for (int i = 0; i <30 ; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
HashSet底层是通过HashMap实现的,那么hashmap也是线程不安全的
Set<String> set = new HashSet<>();
// 点进去
public HashSet() {
map = new HashMap<>();
}
// add方法 就是map的put方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//private static final Object PRESENT = new Object(); 不变得值
线程不安全的类HashMap
package com.qjx.unsafe;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class UnSafeHashMap {
public static void main(String[] args) {
Map<String,String> map = new HashMap<>(16,0.75f);
for (int i = 0; i <30 ; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
HashMap为什么是线程不安全的?
假设这里有两条线程同时判断到hash桶上的第一个元素是为null,因为都是为null,所以两条线程都执行了插入,这里的话插入快的线程的值就会被其他线程的值所覆盖。
解决的方法ConcurrentHashMap
package com.qjx.unsafe;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class UnSafeHashMap {
public static void main(String[] args) {
//Map<String,String> map = new HashMap<>(16,0.75f);
Map<String,String> map = new ConcurrentHashMap<>();
for (int i = 0; i <30 ; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
在1.7之前的话使用的segment数组+entry数组+分段锁的形式出现,首先会先通过hash算法计算出segment数组的下标,每个segment有自己的锁,所以我们在将元素加入到entry数组的时候就是线程安全的,这样就比HashTable单纯的使用synchroized效率高