1. Callable接口
类似于Runnable接口,Runnable描述的任务,不带返回值;Callable描述的任务带返回值。
public class Test {
//创建线程,计算1+2+...+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<Integer> futureTask = new FutureTask<>(callable);
//Thread构造方法不能直接传callable,需要借助futureTask
Thread t = new Thread(futureTask);
t.start();
//获取线程计算结果
//get方法会阻塞等待直到call方法计算完毕,get才会返回
System.out.println(futureTask.get());
}
}
2. ReentrantLock
和synchronized一样是可重入锁,但是两者又有些不同,可以说reentrantLock是对synchronized的补充。
核心方法
- lock()加锁
- unlock()解锁
- tryLock()尝试加锁
和synchronized相比的缺点
进入synchronized内自动加锁,出了synchronized自动解锁。而reentrantLock需要手动加锁,解锁。这就可能出现解锁失败
public static void main(String[] args) {
ReentrantLock locker = new ReentrantLock();
locker.lock();
if(true) {
return;
}
locker.unlock();
}
上面代码直接return没有执行unlock()方法,解决方法就是使用try finally
public static void main(String[] args) {
ReentrantLock locker = new ReentrantLock();
try {
locker.lock();
if(true) {
return;
}
} finally {
locker.unlock();
}
}
和synchronized相比的优点
- tryLock尝试加锁,可以设置加锁等待时间。而synchronized采用的是“死等策略”,死等需要慎重。
- ReentrantLock可以实现成公平锁,默认是非公平锁
ReentrantLock locker = new ReentrantLock(true);
- synchronized是搭配wait/notify实现等待通知机制,随机唤醒一个等待的线程。ReentrantLock是搭配Condition类实现,可以指定唤醒哪个等待的线程。(实例化多个Condition对象,使用await/signal方法)
面试题:谈谈两者的区别
上面的优点+缺点
补充:synchronized是java关键字,底层是JVM实现的;而ReentrantLock是标准库的一个类,底层基于java实现的
3.原子类
原子类的底层是基于CAS实现的,使用原子类,最常见的场景是多线程计数。例如求服务器有多少并发量。
4.信号量Semaphore
信号量相当于一个计数器,表示可用资源的个数。
信号量的基本操作:
- P操作,申请一个资源
- V操作,释放一个资源
当计数为0的时候,继续P操作,就会阻塞等待到其他线程V操作。
信号量可用视为一个广义的锁,而锁相当于一个可用资源为1的信号量。
public static void main(String[] args) throws InterruptedException {
//3个可用资源的信号量
Semaphore semaphore = new Semaphore(3);
//P操作,申请一个资源
semaphore.acquire();
System.out.println("p操作");
//V操作释放一个资源
semaphore.release();
System.out.println("V操作");
semaphore.acquire();
System.out.println("p操作1");
semaphore.acquire();
System.out.println("p操作2");
semaphore.acquire();
System.out.println("p操作3");
semaphore.acquire();
System.out.println("p操作4");
}
发现“p操作4”没有打印,可用资源只有3,前面已经申请3次了,所以没打印,只能阻塞等待到释放资源。
5.CountDownLatch
直译过来就是“计数关闭门阀”,很难理解对吧?举个例子马上让你通透。
5名选手进行百米比赛的时候,当最后一个选手撞线比赛才结束。使用CountDownLatch也是类似,每个选手撞线的时候,就调用countDown方法,当撞线次数达到选手个数,就认为比赛结束。
在举个例子,使用多线程下载一个很大的文件,就切分成多个部分,每个线程负责下载一个部分,当一个线程下载完毕,就调用countDown方法,当所有线程都下载完毕,整个文件就下载完毕。
public static void main(String[] args) throws InterruptedException {
//10个选手参加比赛
CountDownLatch countDownLatch = new CountDownLatch(5);
//创建10个线程来执行任务
for (int i = 0; i < 5; i++) {
Thread t = new Thread(() -> {
System.out.println("选手出发" + Thread.currentThread().getName());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("选手到达" + Thread.currentThread().getName());
//选手撞线
countDownLatch.countDown();
});
t.start();
}
//阻塞等待,直到所有选手都撞线,才能解除阻塞
countDownLatch.await();
System.out.println("比赛结束");
}