JUC
=> java,util.concurrent
并发
这个包里放的东西都是跟多线程相关的~
类似于Runnable
.
Runnable描述的任务,不带返回值
Callable描述的任务是带返回值的!!
如果多线程完成的任务,希望带上结果,就使用Callable
比较好~
代码实现:
通过Callable
创建线程,来计算 1+2+3+…+1000:
public class TeseDemo {
//创建线程,通过线程来计算 1+2+3+...+1000
public static void main(String[] args) throws ExecutionException, InterruptedException {
//使用Callable来定义一个任务
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 futureTask = new FutureTask<>(callable);
//创建线程,来执行上述任务
//Thread的构造方法,不能直接传callable,还需要一个中间的类
Thread thread = new Thread(futureTask);
thread.start();
//获取线程的计算结果
//get方法会阻塞,直到 call 方法计算完毕, get 才会返回~
System.out.println(futureTask.get());
}
}
FutureTask
存在的意义就是为了让我们能够获取到结果(获取到结果的凭证),比如去食堂吃饭的时候,给你一个号,你的做好了,就喊你的号
线程创建的方式:
- 继承Thread(可以使用匿名内部类,也可以不用)
- 使用 Runnable(可以使用匿名内部类,也可以不用)
- 使用 lambda
- 使用线程池
- 使用 Callable
ReentrantLock
是可重入互斥锁~
ReentrantLock
也是可重入锁. “Reentrant
” 这个单词的原意就是 “可重入”
synchronized
也是可重入锁,但是有一些操作是做不到的,ReentrantLock
是对synchronized
的一个补充
ReentrantLock
核心用法,三个方法:
lock()
加锁unlock()
解锁ReentrantLock locked = new ReentrantLock();
//加锁
locked.lock();
//解锁
locked.unlock();
这两节之间如果有return
或者有异常
就可能导致unlock
执行不到了!
synchronized
则没有这个问题,只要代码出了代码块,就一定执行解锁~~
但是ReentrantLock
的优点比缺点更耀眼,它有一些特定的功能,是synchronized
做不到的:
synchronized
和ReentrantLock
的区别: 缺点+优势
原子类的底层,是基于CAS
实现的,以比较高效的方式完成线程安全的自增自减~
使用原子类,最常见的场景就是使用多线程计数
比如写了个服务器,要计算一共有多少个并发量,就可以通过这样的原子变量来累加~
原子类的使用代码:
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Author YuanYuan
* @Date 2022/9/25
* @Time 21:17
*/
public class TestDemo2 {
public static void main(String[] args) throws InterruptedException {
AtomicInteger count = new AtomicInteger(0);
Thread t1 = new Thread(()-> {
for (int i = 0; i < 50000; i++) {
//相当于count++
count.getAndIncrement();
// //相当于++count
// count.incrementAndGet();
// //相当于count--
// count.getAndIncrement();
// //相当于--count
// count.decrementAndGet();
}
});
Thread t2 = new Thread(()-> {
for (int i = 0; i < 50000; i++) {
//相当于count++
count.getAndIncrement();
// //相当于++count
// count.incrementAndGet();
// //相当于count--
// count.getAndIncrement();
// //相当于--count
// count.decrementAndGet();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
//get()获取到内部的值
System.out.println(count.get());
}
}
运行结果:
可以看见没有任何加锁,借助CAS
,完成了一个原子的累加。
JavaEE多线程中的 单例模式与线程池
信号量这个概念是大佬 迪杰斯特拉 提出来的。
信号量的基本操作是两个:
P
操作,申请一个资源V
操作,释放一个资源
信号量本身是一个计数器,表示可用资源的个数:
P
操作申请一个资源,可用资源数就- 1
V
操作释放一个资源,可用资源数就+ 1
当计数为0的时候,继续
P
操作,就会产生阻塞,阻塞等待到其他线程V
操作了为止~~
基于信号量也是可以实现生产者消费者模型
信号量可以视为是一个更广义的锁!! |
锁就是一个特殊的信号量(可用资源只有一个 1 的信号量)
把互斥锁也看作是计数为1的信号量(取值只有1和0,也叫做二元信号量)
JAVA标准库提供了Semaphore
这个类,其实就是把 操作系统 提供的信号量封装了一下
信号量互斥例子:
可以看到,只打印了4个P
操作,因为可用资源已经没有了,所以在第五个acquire()
处就会发生阻塞,这个阻塞就会一直阻塞,直到其他线程进行释放
当需求中,有多个可用资源的时候,就记得要使用信号量~
P 和 V 是荷兰语,P 和 V 是荷兰语中申请和释放的首字母~
类似于一个跑步比赛 ~
当最后一个选手到达终点,比赛就结束~
使用CountDownLatch
就是类似的效果
使用的时候先设置以下有几个选手~
每个选手撞线了,就调用一下countDown
方法,当撞线的次数达到了选手的个数,就认为比赛结束了~
比如使用多线程完成一个任务~
例如,要下载一个很大的文件,就切分成多个部分,每个线程负责下载其中的一个部分~
当所有的线程都下载完毕,整个文件就下载完毕了~
import java.util.concurrent.CountDownLatch;
public class countdownlatch {
public static void main(String[] args) throws InterruptedException {
//有5份文件需要下载
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
//创建5个线程来执行一批任务
Thread thread = new Thread(()-> {
System.out.println("开始执行任务" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("结束执行任务" + Thread.currentThread().getName());
//记录完成
countDownLatch.countDown();
});
thread.start();
}
//await进行阻塞等待,会等到所有的文件都下载完毕之后,才解除阻塞
countDownLatch.await();
System.out.println("文件下载完成!");
}
}
再举个例子:
比如有一个服务挂了,那么响应回不来了怎么办?
await
操作,是可以指定超时时间
可以加个参数:如果时间到了还没有收到相应,那就不等了~