signed

QiShunwang

“诚信为本、客户至上”

02.集合线程不安全问题+Callable接口+JUC下常用三大辅助类

2020/8/19 21:39:19   来源:

6.集合线程不安全问题

普通的list在多线程的时候会出现这个异常:java.util.ConcurrentModificationException 并发修改异常!

6.1List集合的线程不安全解决方案

并发下,list是线程不安全的,但是有三种方式去解决它

  • 1、List list = new Vector<>();//这个是因为add里面有加锁synchronized
  • 2、List list = Collections.synchronizedList(new ArrayList<>());//这个工具类这个将ArrayList包起来,就可以安全
  • 3、List list = new CopyOnWriteArrayList<>();
package com.list;

import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Created by yj on 2020/8/19 17:17
 */
public class ListTest {
    public static void main(String[] args) {
        
        /**
         * 解决方案;
         * 1、List<String> list = new Vector<>();
         * 2、List<String> list = Collections.synchronizedList(new ArrayList<>());
         * 3、List<String> list = new CopyOnWriteArrayList<>();
         */
        // CopyOnWrite 写入时复制  COW  计算机程序设计领域的一种优化策略;
        // 多个线程调用的时候,list,读取的时候,固定的,写入(覆盖)
        // 在写入的时候避免覆盖,造成数据问题!
        // 读写分离
        List<String> list = new CopyOnWriteArrayList<>();

        for (int i = 1; i <= 10; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            },String.valueOf(i)).start();
        }

    }
}

6.2CopyOnWriteArrayList的源码阅读(为什么线程安全)

关于为什么它线程安全?

观察源码可得,下面结论,这也是他保证线程安全的原因。

1.CopyOnWriteArrayList用了lock锁,在Array上用了volatile。

2.用了四步进行了读写分离的操作。

读写分离防止了多个线程在写入的时候被覆盖。虽然在读取的时候他们都读取的是一个数组,但是写入的时候,有可能同时写的时候,会有覆盖操作,后面写的线程把前面的给覆盖了。而这时候用一个数组先复制下来对他进行写入,写完后,再把结果之间给原数组,这样就没有对原数组之间写入,保证了防止覆盖。

image-20200819175352262

setArray,方法和简单就是直接把赋值的数组给了原数组。

image-20200819175618716

Array则用了volatile保证了线程安全,而且还避免了线程阻塞。

image-20200819175628668

6.3Set集合的安全问题的解决

另外说下set底层都是调用的HashMap,因为hashmap的key是不重复的,所以set的值其实就是key。

  • 1.Set set = Collections.synchronizedSet(new HashSet<>());

  • 2.Set set = new CopyOnWriteArraySet<>();//底层采用的也是ReentrantLock,然后采用了读写分离

    /**
     * Created by yj on 2020/8/19 19:49
     */
    public class SetTest {
        public static void main(String[] args) {
            //Set<String> set = new HashSet<>();
            // hashmap
            // Set<String> set = Collections.synchronizedSet(new HashSet<>());
            Set<String> set = new CopyOnWriteArraySet<>();
    
            for (int i = 1; i <=30 ; i++) {
                new Thread(()->{
                    set.add(UUID.randomUUID().toString().substring(0,5));
                    System.out.println(set);
                },String.valueOf(i)).start();
            }
    
        }
    }
    

image-20200819202125295

6.4Map集合的安全问题的解决

  • Map<String, String> map = new ConcurrentHashMap<>();//底层采用synchronized代码块,然后对于key和value为空的会直接报空指针异常
/**
 * Created by yj on 2020/8/19 20:08
 */
public class MapTest {

    public static void main(String[] args) {
        // new HashMap<>(16,0.75);
        Map<String, String> map = new ConcurrentHashMap<>();

        for (int i = 1; i <=30; i++) {
            new Thread(()->{
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
                System.out.println(map);
            },String.valueOf(i)).start();
        }

    }
}

image-20200819202345285

7.Callable接口(JUC下的)

/**
 * Created by yj on 2020/8/19 20:36
 */
public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {


        new Thread().start(); // 怎么启动Callable

        MyThread thread = new MyThread();
        //这样做的原因主要在于Thread接收Runnable,而FutureTask实现的接口继承自Runnable,所以可以将当前的Callable实现类thread
        //扔进去,这样的话,他就能具有Runnable性质,就可以将其扔进new Thread()中了,new Thread()把他当作一个Runnable处理
        //就很像是对于Callable实现类的创建线程的方式来处理
        FutureTask futureTask = new FutureTask(thread); // 适配类

        new Thread(futureTask,"A").start();
        new Thread(futureTask,"B").start(); // 结果会被缓存,效率高

        Integer o = (Integer) futureTask.get();
        System.out.println(o);

    }
}

class MyThread implements Callable<Integer> {

    @Override
    public Integer call() {
        System.out.println("call()"); // 会打印几个call
        // 耗时的操作
        return 1024;
    }

}
//这样做的原因主要在于Thread接收Runnable,而FutureTask实现的接口继承自Runnable(见下图),所以可以将当前的Callable实现类thread
//扔进去,这样的话,他就能具有Runnable性质,就可以将其扔进new Thread()中了,new Thread()把他当作一个Runnable处理
//就很像是对于Callable实现类的创建线程的方式来处理

类不能多继承,但是接口可以多继承,类能多实现接口,但是只能单继承。

image-20200819204337108

7.JUC下常用三大辅助类

7.1countDownLatch减法计数器

设置一个值,然后在线程执行的时候 countDownLatch.countDown();减去1,等他归零的时候往下执行

/**
 * Created by yj on 2020/8/19 21:01
 */
// 计数器
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        // 总数是6,必须要执行任务的时候,再使用!
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 1; i <=6 ; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+" Go out");
                countDownLatch.countDown(); // 数量-1
            },String.valueOf(i)).start();
        }

        countDownLatch.await(); // 等待计数器归零,然后再向下执行

        System.out.println("Close Door");

    }
}

7.2CyclicBarrier加法计数器

设置一个值,当线程到达这个值的时候就会,返回这个这个加法计数器里面的方法。

/**
 * Created by yj on 2020/8/19 21:01
 */
public class CyclicBarrierDemo {
    public static void main(String[] args) {
        /**
         * 集齐7颗龙珠召唤神龙
         */
        // 召唤龙珠的线程
        CyclicBarrier cyclicBarrier = new CyclicBarrier(8,()->{
            System.out.println("召唤神龙成功!");
        });

        for (int i = 1; i <=7 ; i++) {
            final int temp = i;
          //这儿注意一个lambda想要拿到上面的i,上面必须加final
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"收集"+temp+"个龙珠");
                try {
                    cyclicBarrier.await(); // 等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }

    }
}

7.3Semaphore信号量

这个可以做限流和资源互斥的作用,比如下面这段代码就限制了只能有三个停车位,到了三个车位就等待+1,离开了就-1,其他线程才能进来,要保持总数不变。

/**
 * Created by yj on 2020/8/19 21:02
 */
public class SemaphoreDemo {
    public static void main(String[] args) {
        // 线程数量:停车位! 限流!
        Semaphore semaphore = new Semaphore(3);

        for (int i = 1; i <=6 ; i++) {
            new Thread(()->{
                // acquire() 得到
                try {
                    semaphore.acquire();//获得+1
                    System.out.println(Thread.currentThread().getName()+"抢到车位");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName()+"离开车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release(); // release() 释放-1
                }

            },String.valueOf(i)).start();
        }

    }
}