文章目录
- 一. JUC中常见的类
- 1. Callable接口
- 2. ReentrantLock
- 3. Semaphore 信号量
- 4. CountDownLatch
- 二. 集合类在多线程下的线程安全问题
- 多线程下使用ArrayList
- 多线程下使用哈希表(重要)
下面介绍的内容是面试中常考, 但是实际开发中用不到的知识
一. 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());
}
}
**(重点)总结一下:
线程创建的方式:
- 继承Thread
- 使用Runnable
- 使用lambda
- 使用线程池 / ThreadFactory
- 使用Callable
2. ReentrantLock
是JVM提供的一种锁, 可重入锁
当年早期的JVM中, synchronized没现在这么好用, 当时ReentrantLock还是非常有市场的
随着版本的对待, synchronized越来越强, 基本上遇到加锁的时候, 无脑用synchronized大概率不会出现问题
ReentrantLock是通过lock unlock的方式进行加锁解锁的
这种方式就可能会出现忘记加锁的情况, 所以我们一般搭配try-finally使用
与synchronized不同点有以下三个方面:
- ReentrantLock提供了公平锁的实现
synchronized只是非公平锁
传true就是公平锁的形态, 写false或者不写, 就是非公平锁 - ReentrantLock提供tryLock操作, 给加锁提供了更多可能的空间
tryLock尝试加锁, 如果锁已经被获取到了, 直接返回失败, 而不会继续阻塞等待
tryLock操作还有一个版本, 可以指定等待的时间
而synchronized是遇到锁竞争, 就阻塞等待 - ReentrantLock是搭配Condition类完成等待通知, Condition可以指定线程唤醒
synchronized是搭配wait notify完成等待通知机制, notify只能唤醒等待线程中的一个
3. Semaphore 信号量
信号量就是一个计数器, 描述了可用资源的个数
围绕信号量有两个操作:
- P操作, 计数器-1, 申请资源
- V操作, 计数器+1, 释放资源
在Semaphore类中, P操作使用acquire, V操作使用release
如果申请资源数超过了初始值, 就会等待阻塞, 等其他进程进行V操作
锁, 其实就是特殊的信号量, 如果信号量只有0 1 两个取值, 此时就称为"二院信号量", 本质上就是一把锁
锁, 本质是个可用资源
4. CountDownLatch
当我们把一个任务拆分成很多个的时候, 可以通过这个工具类来识别任务是否整体执行完毕了
如果把一个任务拆分成10个任务, 那么每一个小任务完成后, 我们通过countDown方法来记录
使用await方法可以等待所有小任务结束
也就是await会阻塞等待吗直到countDown调用的次数, 和构造方法指定的此时一致的时候, await会返回
二. 集合类在多线程下的线程安全问题
原来的集合类, ⼤部分都不是线程安全的.
Vector, Stack, HashTable, 是线程安全的(不建议⽤), 自带了synchronized, 其他的集合类不是线程安全的.
但是上述集合类在使用过程中并不一定是线程安全的, 只是每个方法是带锁的
如果需要使用其他类, 就需要手动加锁
但是手动加锁比较麻烦, 标准库中提供了一下其他的解决方案
多线程下使用ArrayList
以ArrayList为例:
使用这个方法, 就相当于给ArrayList这些集合类, 套一层壳, 给关键方法都加上了synchronized- 使⽤ CopyOnWriteArrayList
CopyOnWrite容器即写时拷贝的容器。
当我们往⼀个容器添加元素的时候,不直接往当前容器添加,⽽是先将当前容器进⾏Copy,复制
出⼀个新的容器,然后新的容器⾥添加元素
添加完元素之后,再将原容器的引⽤指向新的容器
但是当我们读的时候, 还是读旧的内容, 就不会读到错误的数据了
所以CopyOnWrite容器也是⼀种读写分离的思想,读和写不同的容器。
优点:
在读多写少的场景下, 性能很⾼, 不需要加锁竞争.
缺点:
1.占⽤内存较多.
2.新写的数据不能被第⼀时间读取到
多线程下使用哈希表(重要)
HashMap是线程不安全的
Hashtable是带锁的, 就是在每个方法头上加上了synchronized, 就相当于是针对this加锁
所以只要是针对Hashtable上的元素进行操作, 就会涉及到锁冲突, 效率是非常低的
想象一下哈希表的结构, 是数组加链表的形式, 如果我们针对不同的链表进行操作时, 是不会线程不安全问题的
ConcurrentHashMap做出了优化
- 使用"桶锁"的方式, 来代替一把"全局锁", 有效降低锁冲突的概率
如果两个线程, 针对不同的链表进行操作, 是不会涉及到锁冲突的
上述的提升效率是非常大的, 因为大部分操作如果没有锁冲突了, 那么synchronized就是个偏向锁 - 引入CAS来修改公共变量
像一些公共变量, 如size, 即使即使插入的是不同链表上的元素, 也会涉及到多线程修改同一个变量
那么引入CAS, 通过CAS的方式, 来修改size, 也就避免了加锁操作 - 针对扩容操作做出了特殊的优化 ---- 化整为零
如果发现, 负载因子太大了, 就需要扩容, 扩容是一个比较低效的操作
普通的HashMap, 要在一次put操作的过程中完成扩容, 就会使put操作非常卡
ConcurrentHashMap会在扩容的时候, 搞两份空间
一份是扩容之前的空间
一份是扩容之后的空间
接下来, 没戏进行hash表的基本操作, 都会把一部分数据从旧空间搬运到新空间, 将数据分成多次搬
插入操作: 插入到新空间
删除操作: 新的旧的都要删除
查找操作: 新的旧的都要查找