JUC(java.util.concurrent)是Java提供的一个并发编程的工具包,里面包含了很多在并发环境下好用的工具,本篇主要来介绍JUC里几个常用的工具。
目录
Callable接口
在我们前面介绍的创建线程的方式中,无论是继承Thread类还是实训Runnable接口,都是通过重写run方法来定义线程所要执行的认为,但通过run方法我们无法获取线程执行完毕后的返回值,那如果遇到需要获取返回值的场景时该怎么办呢?此时就得考虑使用Callable接口了。接下来,我们具体来看一下Callable的使用方法。
通过自定义的一个实现Callable接口的类,我们可以发现,实现Callable需要重写call方法
import java.util.concurrent.Callable;
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return null;
}
}
call方法和run方法大体相同,都是描述线程具体要执行的认为,但是call方法是有返回值的,返回值的类型可以通过修改Callable的泛型参数来做出调整。
Callable并不能直接作为构建Thread类的参数,构建Thread类我们需要先创建另外一个类FutureTask,然后通过FutureTask来创建Thread,具体代码如下:
public void test01(){
//以匿名内部类的方式创建Callable的实现类
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
//返回1
return 1;
}
};
//将callable传入FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(callable);
//将futuretask传入Thread
Thread t = new Thread(futureTask);
}
在FutureTask中有一个get方法,通过get方法就能获取到线程返回的参数,不过要注意的是在调用get方法时,会抛出一个打断异常,这是因为如果相关线程还没有执行完,当前线程就会进行阻塞等待,直到相关线程执行完毕并返回数据,当前线程才能继续执行并拿到返回的数据。完善代码后如下:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test {
public void test01() throws ExecutionException, InterruptedException {
//以匿名内部类的方式创建Callable的实现类
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int i =1 ;
//返回i
System.out.println("线程t返回了数据:" + i);
return i;
}
};
//将callable传入FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(callable);
//将futuretask传入Thread
Thread t = new Thread(futureTask);
t.start();
int i = futureTask.get();
System.out.println("主线程获得了返回数据:"+ i);
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Test test = new Test();
test.test01();
}
}
运行结果
ReentrantLock
ReenTrantLock是Java标准库为我们提供的另一把可重入锁,它和synchronize最大的差异就是加锁方式的不同,ReenTrantLock是基于代码进行加锁解锁的,需要自己通过写代码来解释和加锁,而synchronize则是基于代码块来加锁解锁的,进代码块自动加锁,出代码块自动解锁,无需手动写代码。ReenTrantLock中主要有以下几种方法
方法 | 作用 |
lock | 加锁 |
trylock(time) | 有最大时间的加锁 |
unlock | 解锁 |
使用时通常以下面这种方式
ReentrantLock lock = new ReentrantLock();
lock.lock();
//要加锁的代码
lock.unlock();
这里的lock对象就是进行加锁的对象
使用这种方式其实是有一定安全隐患的,如果在加锁的代码中出现异常,就会导致代码还没有执行到unlock方法就提前结束了,为了避免这个问题我们通常需要在代码外面再加一层try-catch,并将unlock放在finally中具体如下:
ReentrantLock lock = new ReentrantLock();
try {
lock.lock();
//代码
}
catch (Exception e){
}
finally {
lock.unlock();
}
ReentrantLock是可以实现公平锁的只需要在创建时将参数设为trure即可
ReentrantLock lock = new ReentrantLock(true);
在synchronize中唤醒线程时只能随机唤醒,而使用ReentrantLock 则可以通过搭配Condition类来实现指定线程唤醒。
信号量Semaphore
信号量可以理解成一个计数器,他表示当前可以资源的数量,他有两个操作
- P操作:申请一个资源
- V操在:释放一个资源
信号量就像是在停车场经常能看到的当前剩余车位数量的告示牌,当有车子进来时,告示牌上的数字就减一(P操作),当有车子出来时,告示牌上的数字就加一(V操作),如果当前车位为0则不能再进来车(P操作阻塞),如果当前车库里没车(即信号l量是满的),则不能再出来车(V操作阻塞)
在Java中,Semaphore类对信号量进行了封装,其中acquire方法就对应了P操作,release方法就对应了V操作,接下来让我们通过Semaphore类来模拟一下前面的停车场景
import java.util.concurrent.Semaphore;
public class SemaphoreTest {
public void test01(){
//创建Semaphore类,并设置大小,表示停车场一共有多少车位,这里设置10个
Semaphore semaphore = new Semaphore(10);
//创建认务Runnable,表示一辆汽车出入停车场的过程
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
//进入停车场
System.out.println("进入车库,车位减少一个,车位数为"+ semaphore.getQueueLength());
semaphore.acquire();
//停车
Thread.sleep(1000);
//出停车场
System.out.println("出车库,车位增加一个车位数为:"+ semaphore.getQueueLength());
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//创建20个线程,分别模拟20辆车,让这20辆车执行上面的任务
for (int i = 0; i < 20; i++) {
Thread t = new Thread(runnable);
t.start();
}
}
public static void main(String[] args) {
SemaphoreTest test = new SemaphoreTest();
test.test01();
}
}
运行结果
使用Semaphore还能模拟实现一个简单的锁,将信号量初始值设为1,如果信号量为1,则代表无人加锁,如果为0则代表被加锁 代码如下
Semaphore semaphore = new Semaphore(1);
public void lock() throws InterruptedException {
semaphore.acquire();
}
public void unlock(){
semaphore.release();
}
CountDownLatch
赛跑是一种常见体育竞赛,在一次赛跑中,会先设定好参赛人数,比赛开始后,当所有参赛人员都到冲线后,一场赛跑就结束了。CountDownLatch的使用其实就跟赛跑一样,也是先设定好一个数值,当进行设定的数值次冲线后,CountDownLatch就结束了。CountDownLatch有如下几个方法
方法 | 作用 |
CountDownLatch(人数) | 构造方法,参数为参赛人数 |
await() | 进入阻塞等待,直到所有选手冲线 |
countDown() | 表示一个选手冲线 |
下面我们来具体使用一下CountDownLatch
import java.util.concurrent.CountDownLatch;
public class CountDownLatchTest {
//创建CountDownLatch,设置参赛人数为5
CountDownLatch countDownLatch = new CountDownLatch(5);
public void test01() throws InterruptedException {
//设置五个线程代表五个参赛选手
for (int i = 0; i < 5; i++) {
int j = i;
Thread t = new Thread(() ->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("选手" + j + "冲线");
countDownLatch.countDown();
});
t.start();
}
//等待所有选手冲线
countDownLatch.await();
System.out.println("所有选手已冲线,比赛结束");
}
public static void main(String[] args) throws InterruptedException {
CountDownLatchTest test = new CountDownLatchTest();
test.test01();
}
}
运行结果:
CountDownLatch还可以应用到文件下载的场景里,将要下载的内容分成多个部分,然后创建多个线程同时下载,这样下载的速度就能成倍提升,通过CountDownLatch就能判断是否所有线程都下好了,如果都下好了,就被每个线程下载的整合在一起。