狂神说JAVA-JUC并发编程(二)线程不安全的集合、JUC常用的辅助类

总结知识点

小狂神的学习方法推荐:1、先会用、2、货比3家,寻找其他解决方案,3、分析源码!
Arraylist的foreach遍历方法
生成随机字符串的方法:UUID(Universally Unique Identifier):通用唯一识别码
并发下 ArrayList 不安全的解决方案;Collections工具类下有同步的集合,比如SynchronizedList

线程不安全的集合

CopyOnWriteArrayList

Arrarlist测试(单线程)

public class ListTest {
    
    
    public static void main(String[] args) {
    
    
        List<String> list= Arrays.asList("1","2","3");
        list.forEach(System.out::println);
    }
}

在这里插入图片描述

多线程下ArrayList还安全吗?
现在我们创建10个线程来向List添加元素

 public static void main(String[] args) {
    
    

        List<String> list=new ArrayList<>();
        for (int i = 0; i <= 10; i++) {
    
    
            new Thread(()->{
    
    
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            },String.valueOf(i)).start();
        }
        //UUID(Universally Unique Identifier):通用唯一识别码,是一种软件建构的标准.UUID是指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的
        //String.valueOf(i),给10个线程分别取名字

测试结果:
如果不加 System.out.println(list);
在这里插入图片描述

如果加上 System.out.println(list);
出现并发修改异常

Exception in thread “10” java.util.ConcurrentModificationException

在这里插入图片描述
并发下 ArrayList 不安全的解决方案
1、List list = new Vector<>();//vector默认是安全的
2、List list = Collections.synchronizedList(new ArrayList<>
());
3、List list = new CopyOnWriteArrayList<>();//CopyOnWriteArrayList,写入时复制

CopyOnWrite 写入时复制 COW 计算机程序设计领域的一种优化策略;
多个线程调用的时候,list,读取的时候,固定的,写入(覆盖)
在写入的时候避免覆盖,造成数据问题!
读写分离(写入的时候复制一个数组出来,写入完之后再插入进去,保证线程安全)

CopyOnWriteArrayList 比 Vector Nb 在哪里?

Vector的add方法有Synchronized修饰(看源码),有Synchronized修饰的方法,效率都比较低

 public synchronized boolean add(E e) {
    
    
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

CopyOnWriteArrayList的add方法用的是Lock锁(看源码)

 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();
        }
    }

CopyOnWriteSet

多线程下HashSet安全测试?

 public static void main(String[] args) {
    
    
       Set<String> set = new HashSet<>();
       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 不安全的解决方案

一个是通过工具类转化成Synchronized,另一个是写入时复制,保证效率跟性能问题
1 Set set = Collections.synchronizedSet(new HashSet<>());
2 Set set=new CopyOnWriteArraySet<>();

HashSet的底层是什么?
答出来不会觉得你很牛逼,但是答不出来就会觉得你不行

可以看到,HashSet的底层就是HashMap

 public HashSet() {
    
    
        map = new HashMap<>();
    }

HashSet的add方法?
本质就是map的key,因为map的key是不能重复的,所以set是无序的,也是无法重复的

 public boolean add(E e) {
    
    
        return map.put(e, PRESENT)==null;
    }

在这里插入图片描述

ConcurrentHashMap

 HashMap<String, String> objectObjectHashMap = new HashMap<>();

按住ALT+7,我们可以查看这个HashMap的Java文件中所有的方法
在这里插入图片描述

问题1:Map是这样用的吗?

答:工作中不这样用

问题2:map默认等价于什么

其中16是初始容量,0.75是加载因子

HashMap<String, String> objectObjectHashMap = new HashMap<>(16,0.75);

注意是位运算

 /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

多线程下HashMap的安全测试?

 public static void main(String[] args) {
    
    
        HashMap<String, String> map = new HashMap<>();
        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 不安全的解决方案

1 Map<String, String> map=new ConcurrentHashMap<>();
2 Map<String, String> map= Collections.synchronizedMap(new HashMap<>());
没有CopyOnWriteMap

Callable

官方文档:Callable接口类似于Runnable ,因为它们都是为其实例可能由另一个线程执行的类设计的。 然而, Runnable不返回结果,也不能抛出被检查的异常。

1可以由返回值
2可以跑出异常
3 方法不同,run()/call()

根据底层源码

public Thread(Runnable target) {
    
    
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

Thread 只能接受Runnable类型的参数,不能接受Callable类型的参数
但是,Runnable有一个实现类

Class FutureTask<V>

在这里插入图片描述FutureTask可以接受Callable类型的参数

所以我么可以通过new Thread启动Callable

public class CallableTest {
    
    

    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    

        MyThread myThread = new MyThread();
//适配类:FutureTask
        FutureTask futureTask = new FutureTask(myThread);
        new Thread(futureTask,"A").start();

        //获取返回值,Callable的返回结果
       Integer result= (Integer )futureTask.get();
       /*get方法可能会产生阻塞,如果Callable是一个耗时的操作,get()会等线程执行完才获取结果
       * 所以,get方法一般放在最后,或者使用异步通信
       *
       * */
        System.out.println(result);
    }
}

class MyThread implements Callable<Integer>{
    
    

    @Override
    public Integer call() throws Exception {
    
    
        System.out.println("call()");
        return 1024;
    }
}

问题:如果new两个线程会打印几个call()?

  new Thread(futureTask,"A").start();
  new Thread(futureTask,"B").start();

答:1个
分析:结果会被缓存,效率高
在这里插入图片描述

常用的辅助类(必会)

CountDownLatch(必会)

允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。
CountDownLatch用给定的计数初始化。 await方法阻塞,直到由于countDown()方法的调用而导致当前计数达到零,之后所有等待线程被释放,并且任何后续的await 调用立即返回。 这是一个一次性的现象 -计数无法重置。 如果您需要重置计数的版本,请考虑使用CyclicBarrier 。
在这里插入图片描述

//计数器
public class CountDownLatchTest {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    

        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,每走一个线程就让  countDownLatch减1
            },String.valueOf(i)).start();
        }
        countDownLatch.await();//等待计数器归0,再向下执行
        System.out.println("关门");

    }
}

countDownLatch.countDown(); // 数量-1
countDownLatch.await(); // 等待计数器归零,然后再向下执行
每次有线程调用 countDown() 数量-1,假设计数器变为0,countDownLatch.await() 就会被唤醒,继续
执行!
如果 countDown()没有减到0,后面的程序是不会执行的

 CountDownLatch countDownLatch = new CountDownLatch(8);//倒计时

比如我调到8,但一共只有6个线程(程序死了,结束不了)
在这里插入图片描述

CylicBarrier

如果把CountDownLatch看做一个减法计数器,那么CylicBarrier就是一个加法计数器

在这里插入图片描述

public class SylicBarrierTest {
    
    
    public static void main(String[] args) {
    
    
        CyclicBarrier cyclicBarrier=new CyclicBarrier(7,()->{
    
    
            System.out.println("计数达到7,召唤神龙成功");
        });
        for (int i = 0; i < 7; i++) {
    
    //计数器达到7才会召唤神龙成功,所以我们先创建7个线程
            final int temp=i;//我们想操作i,但是lambda表达式不能直接操作i,所以我们用一个中间变量
            new Thread(()->{
    
    
                System.out.println(Thread.currentThread().getName()+"收集"+(temp+1)+"颗龙珠");
                try {
    
    
                    cyclicBarrier.await();//计数器加到7才会向下执行
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
    
    
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
Thread-0收集1颗龙珠
Thread-1收集2颗龙珠
Thread-2收集3颗龙珠
Thread-3收集4颗龙珠
Thread-4收集5颗龙珠
Thread-5收集6颗龙珠
Thread-6收集7颗龙珠
计数达到7,召唤神龙成功

Semaphore(信号量)

package com.kuang.add;

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SemphoreDemo {
    
    
    public static void main(String[] args) {
    
    
       //模拟停车,假设现在有6辆车,但是只有3个停车位
       //在有限的情况下使其有秩序,限流的时候可以使用
        Semaphore semaphore = new Semaphore(3);
        for (int i = 1; i <=6; i++) {
    
    
            new Thread(()->{
    
    
                try {
    
    
                    semaphore.acquire();//因为信号量是3,所以最开始有3个车可以进来,然后等进来的车出去,其他的车又可以进来
                    System.out.println(Thread.currentThread().getName()+"抢到车位");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName()+"离开车位");
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }finally {
    
    
                    semaphore.release();//释放
                }
            },String.valueOf(i)).start();

        }
    }
}

在这里插入图片描述

原理:
semaphore.acquire() 获得,假设如果已经满了,等待,等待被释放为止!

semaphore.release(); 释放,会将当前的信号量释放 + 1,然后唤醒等待的线程!
作用: 多个共享资源互斥的使用!并发限流,控制最大的线程数!

猜你喜欢

转载自blog.csdn.net/ningmengshuxiawo/article/details/113366774