Callable 是一个接口(interface),相当于把线程封装成了一个“返回值”,方便程序员借助多线程的计算结果。并且是一种创建线程的方式。
在Callable这个接口中,还是重写了一个call()方法,用来描述线程执行的任务,在完成结果之后,可以返回一个计算结果。但是我们的Runnbale 就不太适合 让线程计算出一个结果。
例如:分别使用两种方法创建一个线程,让这个线程计算1 + 2 + 3 + …+ 1000
Runnable 来实现就会比较麻烦
public class TestDemo2 {
static class Result{
public int sum = 0;
public Object locker = new Object(); //创建锁对象
}
public static void main(String[] args) throws InterruptedException {
Result result = new Result();
Thread t = new Thread(){
@Override
public void run() {
int sum = 0;
for(int i = 1;i<=1000;i++){
sum += i;
}
synchronized (result.locker){
result.sum = sum;
result.locker.notify();
}
}
};
t.start();
synchronized (result.locker){
while(result.sum == 0){
result.locker.wait();
}
}
System.out.println(result.sum);
}
}
可以看出,上述代码需要一个辅助类Result
,还需要使用一系列的加锁
和wait notify 操作
,代码复杂,容易出错。
使用Callable接口,重写Callbale接口中的call()方法
重写Callanle的call()方法
,完成累加的过程,直接通过返回值返回计算结果。把callable 实例使用的FutureTask包装一下
创建线程,线程的构造方法传入 FutureTask,此时新线程就会执行FutureTask内部的Callable的call()方法,完成计算,计算的结果就放到FutureTask对象中
funtureTask.get()
能够阻塞等待新线程计算完毕,并获取带FutureTask中的结果。import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class TestDemo3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = 1;i<=1000;i++){
sum += i;
}
return sum;
}
};
FutureTask<Integer> task = new FutureTask<>(callable);
Thread t = new Thread(task);
t.start();
System.out.println(task.get());
}
}
理解Callable
Callable 和 Runnable 相对,都是描述一个任务,Callable 描述的是带有返回的任务,Runnable 描述的是不带有返回值的任务。
Callable 通常需要配合FutrureTask 来使用。FutureTask 用来保存 Callable 的返回结果。因为Callable往往是在另一个线程中执行的,啥时候执行完并不确定。
FutureTask 就可以负责这个等待结果出来的工作。
ReentrantLock(可重入锁):可重入互斥锁,和synchronized的定位类似,都是用来实现互斥的效果,保证线程安全。
ReentranLock 的用法:
lock() 加锁,如果获得不到锁就死等
unlock() 解锁
把加锁和解锁两个操作分开,那么ReentrantLock是把锁的两个操作分开好呢和时synchronized把所得两个操作和到一起好呢?
这种分开的做法不太好,很容易遗漏unlock(),容易出现死锁,当多个线程竞争同一把锁的时候,会发生严重的阻塞。因为忘记了解锁。
所以就要了一种方式,保证不管是否异常都能执行到unlock()
public class TestDemo4 {
public static void main(String[] args) {
ReentrantLock locker = new ReentrantLock();
try{
locker.lock(); //加锁
}finally {
locker.unlock(); //解锁
}
}
}
如何选择使用那个锁:
Semaphore(信号量):是一个广义的锁,锁是信号量里的一种特殊情况,叫做“二元信号量”,信号量,用来表示“可用资源个数”,本质上就是一个计数器。
正确理解信号量:
可以把信号量想象成为停车场的展示牌,当前停车场的车位有100个,表示有100个可用资源。当有车开进去的时候,就相当于申请了一个可用资源。
这时候100个可用资源的1个已经被占用了,可用车位数 - 1
(这个称为信号量的P操作)。当有车开出去,就相当于释放了一个可用资源,那么么此时的可用车位 + 1
(这里称为信号量的V操作),如果计数器的值已经为0了,当时此时还尝试申请资源,那么就会发生阻塞等待,直到有其他线程资源的释放。
但是有些老铁就会问上述所说的 P 操作 和 V操作,里面的 P 和 V 分别是那个因为单词的缩写呢?
其实提出信号量的老哥是一个 芬兰人 叫 地杰斯特拉(数学家) 如果老铁们熟悉数据结构中的 图 (这里面有一著名的算法—地杰斯特拉算法—>能够计算两点之间的最短路径),这里的P 和 V 是芬兰语的单词缩写 英文 :P —> acquire(申请) V —> relese(释放)
我们熟知的 “锁” 就可以视为一个“二原信号量”,可用资源就一个,计数器的取值,非0 即 1
例如以下代码:
CountDownLatch(闭锁)
主要功能:同时等待N个任务执行结束。
举一个例子: 就好比跑步比赛,10个选手依次就位。哨声向才能出发,所有的选手都通过终点,才能公布成绩。
在CountDownLatch类中提供了这两种方法:countDown() 和 await()方法
countDown()
给每个线程里面器调用,就表示这个线程中的任务执行结束了
await()
是等待线程去调用,当所有线程中的任务都执行完时,await() 就从阻塞中返回,就表示所有线程中的任务都执行完成了。==当调用 countDown()方法 的次数和线程的总个数一直的时候,那么此时就会有await()返回的情况
线程同步的方式有哪些?
synchronized,ReetranLock,Semaphore 等都可以用于线程同步
为什么有了 synchronized 还需要 JUC下的lock?
以 JUC 的 ReentrantLock为例:
- synchronized 使用是不需要手动的释放锁, ReentrantLock 使用是需要手动释放,使用起来更灵活。
- synchronized 在申请锁失败时,会死等,ReetrantLock 可以通过trylock的凡是等待一段时间就放弃。
- synchronized 是非公平锁,ReetrantLock 默认是非公平锁,可以通过构造方法一如一个true开启 公平锁模式
- synchronized 是通过Object 的wait/notify 实现等待 唤醒,每次唤醒的是一个随机等待的线程。ReetrantLock 搭配Condition 类实现等待 唤醒 可以更精确控制唤醒每个指定的线程。
信号量听说过吗?之前都用在那些场景下?
信号量,用于表示“可用的资源的个数”,本质上是一个计数器
使用信号量可以实现 “共享锁”,比如某个资源允许3个线程同时使用,那么就可以使用 P 操作作为加锁,V 操作做为解锁,前三个线程的 P 操作,都能顺利返回,后续线程在进行 P 操作就会阻塞等待,直到前面的线程执行了V 操作。
原来的集合类,大部分都不是线程安全的
Vector,Stack,HashTable 是线程安全的(但是不建议使用)其他的集合类不是线程安全的
1.使用同步机制(synchronized 或者 ReetrantLock) 用 关键字或者类修饰ArrayList
2.Collections.synchronizedList(new ArrayList)
synchronizedList 是一个标准库中提供的一个基于 synchronized 进行线程同步的List
synchronizedList 的关键操作上都带有 synchronized
这种就是直接使用synchronized 修饰ArrayList中的类或者方法属于 “无脑加锁”
3.使用 CopyOnWirteArrayList
CopyOnWrite 容器即写时复制的容器。
- 当我们往一个容器中添加元素的时候,不直接往当前的容器添加元素,而是现将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素
- 复制完元素之后,再将原容器里增添元素。
例如:如果咱们是多线程去读这个ArrayList,此时没有线程安全问题,完全不需要加锁,也不需要其他方面的控制,但是如果有多线程去写这个ArrayList,就是把这个ArrayList给复制一份,先去修改副本。
优点: 这样做的好处就是,修改的同时对于读操作,是没有任何影响的,读的时候就会读取原来的旧版本,不会出现,读带一个"修改了一半"的中间版本。也就是说适合于 读多写少的情况,也适合数据小的情况,在我们日常配置数据的时候,经常就会用到这种类似的操作。这种策略也叫做“双缓冲区策略”。操作系统,创建进程的时候,也会通过写时拷贝。显卡在渲染的时候,也是通过类似的机制。
缺点: 占用内存较多,新写的数据不能第一时间读取到。
HashMap 本身是线程不安全的
HashTable 如何保证线程安全的?
就是给关键字加锁
针对 this 加锁,当有多个线程来访问这个HashTable的时候,无论是怎么样的操作,无论是怎么样的数据,都会产生锁竞争。
这样的设计就会导致锁竞争的概率非常大,效率就比较低。
concurrentHashMap的优点:
HashMap: 线程不安全. key 允许为 null
Hashtable: 线程安全. 使用 synchronized 锁 Hashtable 对象, 效率较低. key 不允许为 null.
ConcurrentHashMap: 线程安全. 使用 synchronized 锁每个链表头结点, 锁冲突概率低, 充分利用
CAS 机制. 优化了扩容方式. key 不允许为 null