目录
1. Callable接口
1.1 Callable接口和Runnable接口的区别?
1.2 使用Callable接口编写代码。
2. ReentrantLock 可重入锁
3.信号量 semaphore
3.1 Java中信号量的使用
4.CountDownLatch
JUC: java.util.concurrent -> 这个包里的内容主要是一些多线程,并发编程相关的组件。比如:
1.Callable接口,也是一种创建线程的方式。适合于想让某个线程执行一个逻辑,并且返回一个结果。相比之下,我们熟悉的Runnable(创建任务时常用)不关注结果。Callable接口使用时需要额外搭配一个FutureTask类。
相同点:都是用来定义一个任务,从而去创建出一个线程。
不同点:
public class vvsvvsvs {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建任务 使用匿名内部类的方式
Callable callable = new Callable() { //这个泛型里填的类型就是返回值的类型
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 1000; i++) {
sum += i;
}
return sum;
}
};
//把任务放到线程中执行
//外创建了一个FutureTask 作用是什么?
FutureTask futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
//获取任务返回结果
System.out.println(futureTask.get());
}
}
上面代码中FutureTask的作用:在执行方法的时候,将传入的Callable对象或者Runnable对象封装成FutureTask对象,核心作用是获取有返回值的线程结果。
当FutureTask处于未启动状态时,执行futureTask.get()方法将导致调用线程阻塞。如果futureTask处于已完成状态,调用futureTask.get()方法将导致调用线程立即返回结果或者抛出异常。上面代码中调用线程指主线程。
举个例子:比如我去食堂买饭,付钱后会给我一张小票,后面要凭这张票取饭,然后后厨就相当于一个线程,就开始执行了,然后我们就要等待,等待人家做好。FutureTask对象就相当于小票,调用get方法会执行这个等待过程(阻塞等待)并在等待完成后返回结果。
ReentrantLock也是一个可重入锁,使用效果上和synchronized类似。其加锁为lock()方法,解锁为unlock()方法,其优势在于:
用法:
import java.util.concurrent.locks.ReentrantLock;
public class test {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
lock.lock(); //加锁
try{
//.....
}finally{
lock.unlock(); //解锁 一定要注意解锁 容易忘记
}
}
}
虽然ReentrantLock优势挺多,但是同时ReentrantLock的使用也更加复杂,尤其是容易忘记解锁。所以在加锁的时候,还是首选synchronized。
两个可重入锁的区别?
信号量:操作系统中比较重要的概念。用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作。
信号量其实就是一个变量,也可理解为是一个计数器,它描述了系统"可用资源"的个数,即某一种系统可用资源的剩余个数,比如:系统中只有一台打印机,就可以设置一个初值为1的信号量。
操作:
- 每次申请一个可用资源,就需要让这个变量(计数器) - 1,称为P操作。
- 每次释放一个可用资源,就需要让这个变量(计数器) + 1,称为V操作。
这里的+1,-1操作都是原子的。对信号量的操作基本有三种,即初始化,P操作,V操作。提出信号量的大佬是迪杰斯特拉。
假设初始条件下,某一个信号量是2,每次进行P操作,就会-1,进行2次操作后,信号量就是0了,此时如果我还继续进行P操作,就会进行阻塞等待。
这里的阻塞等待和我们学过的锁有点相似。其实锁本质上就是属于一种特殊的信号量。我们给锁初始化可用资源数为1,那么加锁操作就会-1,减到0后无可用资源了,就会阻塞等待,直到解锁(释放锁)操作执行就会+1,属于是一种二元信号量。
Java中对应的P操作是acquire()方法,V操作是reserve()方法。开发中如果用到了需要申请资源的场景,就可以使用信号量来实现了。
public class test {
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(4); // 4为初始化信号量个数
semaphore.acquire(); // P操作
System.out.println("P操作3");
semaphore.acquire();
System.out.println("P操作2");
semaphore.acquire();
System.out.println("P操作1");
semaphore.acquire();
System.out.println("P操作0");
//由于初始化信号量为4,上面已经连续4次P操作,信号量为0,下面这次P操作会进入阻塞等待
semaphore.acquire();
}
}
这个东西主要是适用于多个线程来完成一系列任务的时候,用来衡量任务的进度是否完成。比如需要把一个任务拆分成多个小的任务,让这些小的任务并发的去执行(多线程)。就可以使用CountDownLatch来判定当前这些小任务是否都完成了。适合于将一个大任务拆分成多个小任务的场景下。
举个例子:当我们要下载一个文件,使用某些工具没充会员时下载速度会很慢,但是当你充会员后下载,速度会成倍的提升。极有可能是没充会员时线程和资源服务器之间只有一个连接,这样传输速度就会受到限制。而充了会员后为何就能下载如此之快,它把下载文件这个大任务,分割成多个小任务,再使用多线程进行下载。
使用CountDownLatch的两个主要方法 await 和 countDown:
await方法:调用的时候就会阻塞,就会等待其他线程完成任务,所有的线程都完成了任务之后,此时这个await才会返回,并继续往下执行。
countDown方法: 告诉countDownLatch,我当前这一个子任务已经完成了。
CountDownLatch在初始化时,需要指定一个整数作为计数器。当调用countDown方法时,计数器会被减1,;当调用await方法时,如果计数器大于0时,线程会被阻塞,一直阻塞到计数器被countDown方法减到0时,线程才会继续执行。计数器是无法重置的,当计数器被减到0时,调用await方法都会直接返回,继续向下执行。
public static void main(String[] args) throws InterruptedException {
CountDownLatch count = new CountDownLatch(10); //初始化10个
for(int i=0;i<10;i++) {
int id = i;
Thread thread = new Thread(() -> {
System.out.println("Thread"+id);
count.countDown();//调用一下,一个子线程完成 并且初始化数-1
});
thread.start();
}
count.await(); // 数字大于0时,调用await会阻塞等待 直到初始化数--为0时,继续往下执行
System.out.println("所有线程都执行完了!");
}
注意:如果我初始化值设为10,但才调用了countDown方法不到10次,线程就会一直等待阻塞下去。如果不想一直等待下去,可以使用另一个带参的await方法。
count.await(3,TimeUnit.SECONDS); //参数为 数字 + 单位