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”
五、相关面试题