JUC不安全及处理
证明集合类不安全
举例
错误:java.util.ConcurrentModificationException
ArrayList在迭代的时候如果同时对其进行修改就会抛出java.util.ConcurrentModificationException异常
称为“并发修改异常”;
不同线程对同一个list进行操作,又要读又要写。
原理
30个线程对list进行写操作
List<String> list = new ArrayList<>();
for (int i = 0; i <30 ; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
看ArrayList的源码
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
没有synchronized线程不安全
解决方法
1、Vector
List<String> list = new Vector<>();
看Vector的源码
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
有synchronized线程安全
2、Collections工具类
List<String> list = Collections.synchronizedList(new ArrayList<>());
Collections提供了方法synchronizedList保证list是同步线程安全的
HashMap,HashSet线程不安全。有同样的线程安全方法
3、写时复制
List<String> list = new CopyOnWriteArrayList<>();
写时复制
对list操作的方法不加锁,性能提升,但出错;加锁,性能下降,但数据一致。
CopyOnWriteArrayList定义
CopyOnWriteArrayList是arraylist的一种线程安全变体,其中所有可变操作(add、set等)都是通过生成底层数组的新副本来实现的。
写操作是通过操作副本来实现的。
CopyOnWrite理论
思想:写时复制+读写分离
CopyOnWrite容器进行并发的读
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#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();
}
}
CopyOnWrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加,
而是先将当前容器Object[]进行Copy,复制出一个新的容器Object[] newElements,然后向新的容器Object[] newElements里添加元素。
添加元素后,再将原容器的引用指向新的容器setArray(newElements)。
这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
扩展
1、HashSet的线程安全版本:CopyOnWriteArraySet
HashSet的底层结构:HashMap
为什么HashMap的add是放k,v键值对,但是HashSet是放一个值?
HashSet的v是一个object对象
public HashSet() {
map = new HashMap<>();
}
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==/2null;
}
2、HashMap的线程安全版本:ConcurrentHashMap
Map<String,String> map = new HashMap<>();//线程不安全
Map<String,String> map = new ConcurrentHashMap<>();//线程安全
Callable接口
Callable接口是什么?
函数式接口:这是一个函数式接口,因此可以用作lambda表达式或方法引用的赋值对象。
获得多线程的几种方式:
- 传统的方式是继承thread类和实现runnable接口
- java5以后还有实现callable接口和java线程池
callable和runnable的对比
创建新类MyThread实现runnable接口
class MyThread implements Runnable{
@Override
public void run() {
}
}
新类MyThread2实现callable接口
class MyThread2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
return 200;
}
}
区别:
- runnable重写的run方法无返回值,callable重写的call方法有返回值
- run方法不抛出异常,call方法抛出异常
- 要重写的方法不一样,run和call
Callable接口的使用
能直接替换runnable吗?
由于线程启动是要将实现了runnable接口的类作为Thread的参数。
但是Thread类的构造方法没有Callable。不能将runnable直接替换为callable。
使用方法
通过FutureTask的方式,一个类实现多个接口。
FutureTask的实例对象实现callable,再把thread作为FutureTask的参数。
FutureTask<Integer> ft = new FutureTask<Integer>(new MyThread());
new Thread(ft, "AA").start();
//获取返回值
ft.get();
FutureTask
是什么
main方法执行时,会被某个方法阻塞(该方法计算耗时久),导致该方法下方(程序逻辑执行顺序)不能够及时执行。
举例:
(1)老师上着课,口渴了,去买水不合适,讲课线程继续,我可以单独起个线程找班长帮忙买水,水买回来了放桌上,我需要的时候再去get。
(2)4个同学,A算1+20,B算21+30,C算31*到40,D算41+50,FutureTask单起个线程给C计算,先汇总ABD,最后等C计算完了再汇总C,拿到最终结果。
原理
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成,当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。
一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务(除了耗时计算以外的计算)后,再去获取结果。
仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。
只计算一次(实现callable的futureTask用于创建两个名称不同的线程,只进入一次call方法,返回一个值)get方法放到最后
示例
FutureTask实现了Callable接口的匿名内部类,且用lambda表达式写法。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
class MyThread implements Runnable{
@Override
public void run() {
}
}
class MyThread2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+"come in callable");
return 200;
}
}
public class CallableDemo {
public static void main(String[] args) throws Exception {
//FutureTask<Integer> futureTask = new FutureTask(new MyThread2());
FutureTask<Integer> futureTask = new FutureTask(()->{
System.out.println(Thread.currentThread().getName()+" come in callable");
TimeUnit.SECONDS.sleep(4);
return 1024;
});
FutureTask<Integer> futureTask2 = new FutureTask(()->{
System.out.println(Thread.currentThread().getName()+" come in callable");
TimeUnit.SECONDS.sleep(4);
return 2048;
});
new Thread(futureTask,"zhang3").start();
new Thread(futureTask2,"li4").start();
//System.out.println(futureTask.get());
//System.out.println(futureTask2.get());
//1、一般放在程序后面,直接获取结果
//2、只会计算结果一次
while(!futureTask.isDone()){
System.out.println("***wait");
}
System.out.println(futureTask.get());//1024
System.out.println(Thread.currentThread().getName()+" come over");//main线程
}
}