JUC:java.util.concurrent
一、Callable 接⼝
接口 | 方法 | |
---|---|---|
Callable | call,带有返回值 | |
Runnable | run,void | |
所以创建一个线程,希望它给你返回一个结果,那么使用 Callable 更加方便一些 |
比如,创建一个线程,让这个线程计算:1+2+3+4+…+1000=?
//使用Runnable接口
public class Demo {
private static int result;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i;
}
});
t.start();
t.join();
System.out.println(“result=” + result);
}
}
- 当前代码可以解决问题,但不够优雅,必须得引入一个成员变量
result
相比之下,Collable 就可以解决上述问题:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Demo3 {
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 = 0; i < 1000; i++) {
sum += i;
}
return sum;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask); //Thread
t.start();
//后续需要通过 futureTask 来拿到最终的结果
System.out.println(futureTask.get());
}
}
Thread
不能直接传入callable
作为参数,需要传入实例化的Callable
对象callable
实例化一个FutureTask
对象futureTask
,再将这个futureTask
对象创建出线程- 并且后面要拿到
sum
的值,也需要通过futureTask
futureTask
的get
操作是带有阻塞功能的- 当前
t
线程还没执行完,get
就会阻塞 - 直到
t
线程执行完之后,get
才会返回
- 当前
- 比如说你去吃麻辣烫,夹好菜后,服务员会给你一个“号码牌”,方便你后续取餐。这里通过
FutureTask
实例化出的对象futureTask
就相当于是号码牌,你通过Callable
实例出的callable
对象就相当于是你夹的菜。你是通过你夹的菜(callable
)拿到号码牌(futureTask
),最后你想要拿到菜,也得通过号码牌(futureTask
)拿到
创建线程的方式:
- 直接继承 Thread
- 使用 Runnable
- 使用 Callable
- 使用 lambda
- 使用线程池
二、ReentrantLock
- 可重入锁
synchronized
只是Java
提供的一种加锁的方式ReentrantLock
属于经典风格的锁,通过lock
和unlock
方法来完成加锁和解锁
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
ReentrantLock locker = new ReentrantLock();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
locker.lock();
count++;
locker.unlock();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
locker.lock();
count++;
locker.unlock();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count=" + count);
}
}
synchronized 和 ReentrantLock 的差异
实际开发中,大多数情况下,使用 synchronized
即可,但 ReentrantLock
相比于 synchronized
还是有一些差异的
-
synchronized
属于关键字(底层是通过JVM
的C++
代码实现的)
ReentrantLock
测试标准库提供的类,通过Java
代码实现的 -
synchronized
通过代码块控制加锁解锁
ReentrantLock
通过调用lock/unlock
方法来完成,unlock
可能会遗漏,要确保能执行到unlock
(通常会把unlock
放在finally
中) -
ReentrantLock
提供了tryLock
这样的加锁风格(重点)- 前面介绍的加锁,都是发现锁被别人占用了,就阻塞等待
- 而
tryLock
在加锁失败的时候,不会阻塞,而是直接返回,并且通过返回值来反馈是加锁成功还是失败 - 坚持不一定成功,但是放弃了一定轻松~(摆烂态度)
-
ReentrantLock
还提供了公平锁的实现(重点)- 它默认是非公平的,但可以在构造方法中,将参数设为
true
,将其设为公平锁
- 它默认是非公平的,但可以在构造方法中,将参数设为
-
ReentrantLock
还提供了功能更强的“等待通知机制”- 基于
Condition
类,功能要比wait/notify
更强一些
- 基于
三、信号量 Semaphore
也称为“信号灯”(开船的水手,旗语)
你去车库停车,如何知道是否还有空位?
现在的停车场,一般的入口处,就会有一个“电子牌”,会显示有多少个空闲车位
- 有车开进去了,上述的计数 -1
- 有车开出来了,上述的计数 +1
- 如果计数为 0 了,说明没空位了
这里的“电子牌”就像是一个“信号量”,信号量是一个计数器,通过计数器衡量“可用资源”的个数
- 申请资源(acquire):让计数器 -1(“P”操作)
- 释放资源(release):让计数器 +1(“V”操作)
- 如果计数器为 0 了,继续申请,就会出现阻塞
所以信号量的操作也称为“PV 操作”
操作系统本身提供了信号量实现,JVM 把操作系统的信号量封装了一下,我们直接使用就可以了
import java.util.concurrent.Semaphore;
public class Demo5 {
public static void main(String[] args) throws InterruptedException {
//参数为可用资源的个数,计数器的初始值
Semaphore semaphore = new Semaphore(3);
semaphore.acquire();
System.out.println("申请一个资源1");
semaphore.acquire();
System.out.println("申请一个资源2");
semaphore.acquire();
System.out.println("申请一个资源3");
semaphore.acquire();
System.out.println("申请一个资源4");
}
}
//运行结果
申请一个资源1
申请一个资源2
申请一个资源3
- 初始化的信号量为 3
- 申请了三个资源后,没空位了,计数器为 0 了,就堵塞住了
若在这里面释放一次资源,就可以将资源 4 申请进去:
import java.util.concurrent.Semaphore;
public class Demo5 {
public static void main(String[] args) throws InterruptedException {
//参数为可用资源的个数,计数器的初始值
Semaphore semaphore = new Semaphore(3);
semaphore.acquire();
System.out.println("申请一个资源1");
semaphore.acquire();
System.out.println("申请一个资源2");
semaphore.acquire();
System.out.println("申请一个资源3");
semaphore.release();
System.out.println("释放一个资源");
semaphore.acquire();
System.out.println("申请一个资源4");
}
}
//运行结果
申请一个资源1
申请一个资源2
申请一个资源3
释放一个资源
申请一个资源4
平替加锁解锁
- 在需要加锁的时候,可以设置一个信号量,初始化一个资源
- 谁要用的时候就申请这个资源,用完之后再释放
- 这样的话,申请到唯一资源的线程执行操作的时候,就不会有其他的线程进行操作了
import java.util.concurrent.Semaphore;
public class Demo4 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(1);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
try {
semaphore.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count++;
semaphore.release();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
try {
semaphore.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count++;
semaphore.release();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count=" + count);
}
}
- 这种值为 1 的信号量,就相当一个锁,资源要么是 1,要么是 0,所以也称为“二元信号量”
四、CountDownLatch
“锁存器”
很多时候,需要把一个大的任务,拆成多个小的任务,通过多线程/线程池来执行。如何衡量,所有的任务都执行完毕了?
- 比如“多线程下载”
- 浏览器的下载,一般是单线程的,下载速度是有限的(一秒 2-3 MB)
- 但是可以通过多线程的方式提高下载速度
就可以使用专门的下载工具,通过多个线程,和服务器建立多个网络连接(服务器进行网速限制都是针对一个连接做出的限制),那如果我创建 10-20 个线程,那么下载的总速度就能大幅度提高 - 多个线程进行一起操作,每个线程下载一部分,所有线程下载完毕再进行拼装
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo6 {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(4);
//构造方法的个数,就是拆分出来的任务数量
CountDownLatch countDownLatch = new CountDownLatch(20);
for (int i = 0; i < 20; i++) {
int id = i;
executorService.submit(() -> {
System.out.println("下载任务" + id + "开始执行");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("下载任务" + id + "结束执行");
//完毕 over!
countDownLatch.countDown();
});
}
// 当 countDownLatch 收到了 20 个“完成”,所有的任务就都完成了
// await => all wait
// await 这个词是计算机术语,“等待所有”
countDownLatch.await();
System.out.println("所有任务都完成");
}
}
CountDownLatch
一般都是结合线程池进行使用- 借助
CountDownLatch
就能衡量出当前任务是否整体执行结束
上面这些再实际开发中用的布套多,但面试可能问到,特别是“ReentrantLock”和“Semaphore”