JUC_线程安全的集合类与常用辅助类

线程安全的集合类与常用辅助类

我们常用的集合类(ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等),为了实现效率的提高,一般都不会加锁,这样就造成了线程的不安全

集合类

为什么不安全?

首先我们来了解一下什么叫集合是线程不安全的:

当多个并发同时对非线程安全的集合进行增删改的时候会破坏这些集合的数据完整性;
例如:当多个线程访问同一个集合或Map时,如果有超过一个线程修改了ArrayList集合,则程序必须手动保证该集合的同步性。

会报 java.util.ConcurrentModificationException 并发修改异常!

先看list,推荐使用CopyOnWriteArrayList

// java.util.ConcurrentModificationException 并发修改异常!
public class ListTest {
	public static void main(String[] args) {
		// 并发下 ArrayList 不安全的吗,Synchronized;
		/**
		* 解决方案;
		* 1、List<String> list = new Vector<>(); //这个太老了,不推荐,但要知道
		* 2、List<String> list = Collections.synchronizedList(new ArrayList<>()); //这个效率没CopyOnWriteArrayList快
		* 3、List<String> list = new CopyOnWriteArrayList<>();
		*/
		// CopyOnWrite 写入时复制 COW 计算机程序设计领域的一种优化策略;
		// 多个线程调用的时候,list,读取的时候,固定的,写入(覆盖)
		// 在写入的时候避免覆盖,造成数据问题!
		// 读写分离
		// CopyOnWriteArrayList 比 Vector Nb 在哪里?
		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();
		}
	}
}
list为什么不安全?

先看看报错信息java.util.ConcurrentModificationException

在这里插入图片描述

ArrayList 默认数组大小为 10。假设现在已经添加进去 9 个元素了,size = 9。

  • 线程 A 执行完 add 方法中的 ensureCapacityInternal(size+1) 挂起了。
  • 线程 B 开始执行,校验数组容量发现不需要扩容。于是把 “b” 放在了下标为 9 的位置,且 size 自增 1。此时 size = 10。
  • 线程 A 接着执行,尝试把 “a” 放在下标为 10 的位置,因为 size = 10。但因为数组还没有扩容,最大的下标才为 9,所以会抛出数组越界异常ArrayIndexOutOfBoundsException。

另外第二步 elementData[size++] = e 设置值的操作同样会导致线程不安全。从这里可以看出,这步操作也不是一个原子操作,它由如下两步操作构成:

  • elementData[size] = e;
  • size = size + 1;

在单线程执行这两条代码时没有任何问题,但是当多线程环境下执行时,可能就会发生一个线程的值覆盖另一个线程添加的值,具体逻辑如下:

  • 列表大小为 0,即size=0
  • 线程 A 开始添加一个元素,值为 A。此时它执行第一条操作,将 A 放在了 elementData 下标为 0 的位置上。
  • 接着线程 B 刚好也要开始添加一个值为 B 的元素,且走到了第一步操作。此时线程 B 获取到 size 的值依然为 0,于是它将 B 也放在了 elementData 下标为 0 的位置上。
  • 线程 A 开始将 size 的值增加为 1。
  • 线程 B 开始将 size 的值增加为 2。
  • 这样线程 AB 执行完毕后,理想中情况为 size 为 2,elementData 下标 0 的位置为 A,下标 1 的位置为 B。而实际情况变成了 size 为 2,elementData 下标为 0 的位置变成了 B,下标 1 的位置上什么都没有。并且后续除非使用 set 方法修改此位置的值,否则将一直为 null,因为 size 为 2,添加元素时会从下标为 2 的位置上开始。

Map 不安全,推荐使用ConcurrentHashMap

// ConcurrentModificationException
public class MapTest {
	public static void main(String[] args) {
		// map 是这样用的吗? 不是,工作中不用 HashMap
		// 默认等价于什么? new HashMap<>(16,0.75);
		// Map<String, String> map = new HashMap<>();
		// 唯一的一个家庭作业:研究ConcurrentHashMap的原理
		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();
		}
	}
}
为什么HashMap不安全?

先看看报错信息java.util.ConcurrentModificationException

在这里插入图片描述

HashMap在并发执行put操作时,可能会导致形成循环链表,从而引起死循环。

Set 不安全,推荐使用CopyOnWriteArraySet

/**
* 同理可证 : ConcurrentModificationException
* //1、Set<String> set = Collections.synchronizedSet(new HashSet<>());
* //2、
*/
public class SetTest {
	public static void main(String[] args) {
		// Set<String> set = new HashSet<>();
		// 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();
		}
	}
}
我们来了解一下HashSet的本质是什么,就知道他为什么不安全了

我这里值截取了构造函数和add方法

//构造函数 
public HashSet() {
	map = new HashMap<>();   
}
//add方法
public boolean add(E e) {
	return map.put(e, PRESENT)==null;
}

就玩意就是利用了HashMap的key唯一,所以是不可重复的

常用的辅助类

一、CountDownLatch

在这里插入图片描述

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

原理:

countDownLatch.countDown(); // 数量-1

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

二、CyclicBarrier

在这里插入图片描述

public static void main(String[] args) {
    /**
    * 集齐7颗龙珠召唤神龙
    */
    // 召唤龙珠的线程
    CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
    	System.out.println("召唤神龙成功!");
    });
    for (int i = 1; i <=7 ; i++) {
        final int temp = i;
        new Thread(()->{
        	System.out.println(Thread.currentThread().getName()+"收集"+temp+"个龙珠");
        	try {
        		cyclicBarrier.await(); // 等待
        	} catch (InterruptedException e) {
       			e.printStackTrace();
        	} catch (BrokenBarrierException e) {
        		e.printStackTrace();
        	}
        }).start();
    }
}

在这里插入图片描述

原理:

CyclicBarrier相当于一个+1操作

cyclicBarrier.await(); 让 cyclicBarrier+1

如果cyclicBarrier加到了7(我们自己定义的值,就会打印里面的内容)

三、Semaphore

在这里插入图片描述

public static void main(String[] args) {
    // 线程数量:停车位! 限流!
    Semaphore semaphore = new Semaphore(3);
    for (int i = 1; i <=6 ; i++) {
        new Thread(()->{
        	// acquire() 得到
            try {
                semaphore.acquire();
                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() 释放
            }
        },String.valueOf(i)).start();
    }
}

在这里插入图片描述

原理:

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

semaphore.release(); 释放,会将当前的信号量释放 + 1,然后唤醒等待的线程!

作用:

多个共享资源互斥的使用!并发限流,控制最大的线程数!

参考文章:

ArrayList 为什么线程不安全

狂神说—JUC

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我认不到你

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值