[线程]JUC中常见的类 及 集合类在多线程下的线程安全问题


下面介绍的内容是面试中常考, 但是实际开发中用不到的知识

一. JUC中常见的类

JUC : java.util.cincurrent, 存放了很多和多线程相关的组件

1. Callable接口

与Runnable类似, Runnable描述了一个任务, 但是描述的任务run方法没有返回值
Callable也是描述一个任务, 但是call方法有返回值, 表示这个线程执行结束要得到啥结果

在这里插入图片描述
上述代码, 我们把result告知主线程
但是线程内部定义的局部变量是不能被其他线程获取到的
所以我们需要定义一个成员变量过度
在这里插入图片描述
但是这种方式, 就相当于让主线程和t线程耦合过大
那么Callable就可以优雅的解决上述问题
在这里插入图片描述
注意: Callable是不能直接填写在Thread构造方法中, 需要搭配:

在这里插入图片描述
完整代码:

public class Demo33 {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int result = 0;
                for(int i = 1; i <= 1000; i++){
                    result += i;
                }
                return result;
            }
        };
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread thread = new Thread(futureTask);
        thread.start();
        thread.join();
        //使用get获取到返回值
        System.out.println("result = " + futureTask.get());
    }
}

**(重点)总结一下:
线程创建的方式:

  1. 继承Thread
  2. 使用Runnable
  3. 使用lambda
  4. 使用线程池 / ThreadFactory
  5. 使用Callable

2. ReentrantLock

是JVM提供的一种锁, 可重入锁
当年早期的JVM中, synchronized没现在这么好用, 当时ReentrantLock还是非常有市场的
随着版本的对待, synchronized越来越强, 基本上遇到加锁的时候, 无脑用synchronized大概率不会出现问题

ReentrantLock是通过lock unlock的方式进行加锁解锁的
在这里插入图片描述
这种方式就可能会出现忘记加锁的情况, 所以我们一般搭配try-finally使用
在这里插入图片描述
与synchronized不同点有以下三个方面:

  1. ReentrantLock提供了公平锁的实现
    synchronized只是非公平锁
    在这里插入图片描述
    传true就是公平锁的形态, 写false或者不写, 就是非公平锁
  2. ReentrantLock提供tryLock操作, 给加锁提供了更多可能的空间
    tryLock尝试加锁, 如果锁已经被获取到了, 直接返回失败, 而不会继续阻塞等待
    tryLock操作还有一个版本, 可以指定等待的时间
    而synchronized是遇到锁竞争, 就阻塞等待
  3. ReentrantLock是搭配Condition类完成等待通知, Condition可以指定线程唤醒
    synchronized是搭配wait notify完成等待通知机制, notify只能唤醒等待线程中的一个

3. Semaphore 信号量

信号量就是一个计数器, 描述了可用资源的个数
围绕信号量有两个操作:

  1. P操作, 计数器-1, 申请资源
  2. V操作, 计数器+1, 释放资源

在Semaphore类中, P操作使用acquire, V操作使用release
在这里插入图片描述
如果申请资源数超过了初始值, 就会等待阻塞, 等其他进程进行V操作
在这里插入图片描述
锁, 其实就是特殊的信号量, 如果信号量只有0 1 两个取值, 此时就称为"二院信号量", 本质上就是一把锁
锁, 本质是个可用资源

4. CountDownLatch

当我们把一个任务拆分成很多个的时候, 可以通过这个工具类来识别任务是否整体执行完毕了
如果把一个任务拆分成10个任务, 那么每一个小任务完成后, 我们通过countDown方法来记录
使用await方法可以等待所有小任务结束
也就是await会阻塞等待吗直到countDown调用的次数, 和构造方法指定的此时一致的时候, await会返回
在这里插入图片描述

二. 集合类在多线程下的线程安全问题

原来的集合类, ⼤部分都不是线程安全的.
Vector, Stack, HashTable, 是线程安全的(不建议⽤), 自带了synchronized, 其他的集合类不是线程安全的.

但是上述集合类在使用过程中并不一定是线程安全的, 只是每个方法是带锁的
在这里插入图片描述
如果需要使用其他类, 就需要手动加锁
但是手动加锁比较麻烦, 标准库中提供了一下其他的解决方案

多线程下使用ArrayList

以ArrayList为例:

  1. 在这里插入图片描述
    使用这个方法, 就相当于给ArrayList这些集合类, 套一层壳, 给关键方法都加上了synchronized
  2. 使⽤ CopyOnWriteArrayList
    CopyOnWrite容器即写时拷贝的容器。
    当我们往⼀个容器添加元素的时候,不直接往当前容器添加,⽽是先将当前容器进⾏Copy,复制
    出⼀个新的容器,然后新的容器⾥添加元素
    添加完元素之后,再将原容器的引⽤指向新的容器
    但是当我们读的时候, 还是读旧的内容, 就不会读到错误的数据了

所以CopyOnWrite容器也是⼀种读写分离的思想,读和写不同的容器。

优点:
在读多写少的场景下, 性能很⾼, 不需要加锁竞争.
缺点:
1.占⽤内存较多.
2.新写的数据不能被第⼀时间读取到

多线程下使用哈希表(重要)

HashMap是线程不安全的
Hashtable是带锁的, 就是在每个方法头上加上了synchronized, 就相当于是针对this加锁
所以只要是针对Hashtable上的元素进行操作, 就会涉及到锁冲突, 效率是非常低的
想象一下哈希表的结构, 是数组加链表的形式, 如果我们针对不同的链表进行操作时, 是不会线程不安全问题的
ConcurrentHashMap做出了优化

  1. 使用"桶锁"的方式, 来代替一把"全局锁", 有效降低锁冲突的概率
    在这里插入图片描述
    如果两个线程, 针对不同的链表进行操作, 是不会涉及到锁冲突的
    上述的提升效率是非常大的, 因为大部分操作如果没有锁冲突了, 那么synchronized就是个偏向锁
  2. 引入CAS来修改公共变量
    像一些公共变量, 如size, 即使即使插入的是不同链表上的元素, 也会涉及到多线程修改同一个变量
    那么引入CAS, 通过CAS的方式, 来修改size, 也就避免了加锁操作
  3. 针对扩容操作做出了特殊的优化 ---- 化整为零
    如果发现, 负载因子太大了, 就需要扩容, 扩容是一个比较低效的操作
    普通的HashMap, 要在一次put操作的过程中完成扩容, 就会使put操作非常卡
    ConcurrentHashMap会在扩容的时候, 搞两份空间
    一份是扩容之前的空间
    一份是扩容之后的空间
    接下来, 没戏进行hash表的基本操作, 都会把一部分数据从旧空间搬运到新空间, 将数据分成多次搬
    插入操作: 插入到新空间
    删除操作: 新的旧的都要删除
    查找操作: 新的旧的都要查找
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值