JUC不安全及处理和callable接口

本文探讨了Java集合类在多线程环境下的线程安全性问题,详细分析了ArrayList并发修改异常的原因,并介绍了Vector、Collections.synchronizedList、CopyOnWriteArrayList等线程安全集合的实现原理与使用方法。
摘要由CSDN通过智能技术生成

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线程
    }
} 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值