JUC ==> java.util.concurrent
JUC标准库提供的多线程安全相关的包
Callable 接口声明带返回值的任务
类似于Runnable,都是用来描述这个线程的工作的。
Callable描述的任务带返回值,Runnable描述的任务不带返回值
区别:线程封装了一个 “返回值”,Runnable没有返回值。
搭配FutureTask使用
代码示例: 创建线程计算 1 + 2 + 3 + … + 1000,返回结果
public static void main(String[] args) throws InterruptedException, ExecutionException {
//创建Callable任务
Callable<Integer> callable = new Callable<Integer>() {
//call方法描述的是整个线程的任务,带有返回值
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
};
//想要拿到callable里面的返回值,必须创建FutureTask对象
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
int result = futureTask.get();
System.out.println(result);
}
- 创建一个匿名内部类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型.
- 重写 Callable 的 call 方法, 完成累加的过程. 直接通过返回值返回计算结果.
- 把 callable 实例使用 FutureTask 包装一下.
- 创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的
- 启动线程执行call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中.
- 在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果.
FutureTask类似于票根,你得拿着它才能查询到结果。
ReentrantLock
可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.
核心方法:
1、lock()加锁
2、unlock()解锁
3、tryLock() 尝试加锁,能加就加,加不了就算了
缺点:
容易遗漏unlock操作:使用lock()和unlock()时候,有可能中途碰到return语句,unlock还没执行到就返回了。
ReentrantLock locker = new ReentrantLock();
//加锁
lokcer.lock();
//任务 [有可能return]
//解锁
locker.unlock();
解决办法: 使用try finally
ReentrantLock locker = new ReentrantLock();
try {
//加锁
locker.lock();
//任务
} finally {
//解锁
locker.unlock();
}
}
优点:
1、tryLock 是尝试加锁,能加上就加,加不上就放弃,其中tryLock还可以设置加锁的等待(尝试)时间
2、ReentrantLock还可以实现公平锁,默认是非公平
构造的时候传染一个参数就是公平锁ReentrantLock locker = new ReentrantLock(true);
3、synchronized是搭配wait/notify实现等待通知机制,notify唤醒操作是随机唤醒一个等待的线程;
ReentrantLock是搭配Condition类实现的,唤醒操作是可以指定唤醒哪个等待的线程。
【面试题】说说synchronized和ReentrantLock的区别
区别 = 缺点 +优点
- synchronized 是一个关键字, 是 JVM 内部实现的. ReentrantLock 是标准库的一个类, 在 JVM 外实现的(基于 Java 实现).
- synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock.
- synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
- synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式.
- synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.
原子类
原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。
原子类应用
使用原子类,最常见的就是多线程奇数。写了个服务器,服务器一共有多少个并发了,就可以通过原子变量来累加。
- AtomicBoolean
- AtomicInteger
- AtomicIntegerArray
- AtomicLong
- AtomicReference
- AtomicStampedReference
以 AtomicInteger 举例,常见方法有:
addAndGet(int delta); i += delta;
decrementAndGet(); --i;
getAndDecrement(); i–;
incrementAndGet(); ++i;
getAndIncrement(); i++;
ThreadPoolExcutor线程池
之前讲过,不赘述
信号量Semaphore
信号量的基本操作:
P操作 :申请一个资源
V操作:释放一个资源
PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用
信号量本质—计数器
信号量本身是一个计数器,表示可用资源的个数
P操作申请一个资源,可用资源数-1;
V操作,释放一个资源,可用资源数+1;
当奇数为0时候继续P操作,就会阻塞,阻塞等待到其他线程V操作为止。(生产者消费者模型)
信号量是广义的锁
锁就是一个特殊的信号量(可用资源只有1的信号量)
代码示例
创建 Semaphore 示例, 初始化为 4, 表示有 4 个可用资源. acquire 方法表示申请资源(P操作), release 方法表示释放资源(V操作)
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(4);
semaphore.acquire();
System.out.println("申请资源1");
semaphore.acquire();
System.out.println("申请资源2");
semaphore.acquire();
System.out.println("申请资源3");
semaphore.acquire();
System.out.println("申请资源4");
semaphore.acquire();
System.out.println("申请资源5");
semaphore.release();
System.out.println("释放资源");
}
执行效果:
在执行到第四个申请资源以后,代码就阻塞了,原因是一开始的资源就只有4个,在前面已经全部被P操作申请完了,要想再申请,只能等待其他线程的V操作释放资源,所以进行阻塞等待。
CountDownLatch
同时等待 N 个任务执行结束.
举例:跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才算比赛结束。
使用CoutDownLatch就是类似的效果,使用的时候先设置有多少个选手,然后每个选手到达了重点,调用countDown方法,当到达重点的人数到达了先前设置的人数,比赛就认为结束了。
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
//10个选手 相当于10个进程
Thread t = new Thread(()->{
System.out.println("开始执行任务:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("结束执行任务" + Thread.currentThread().getName());
//到达终点
latch.countDown();
});
t.start();
}
latch.await();
System.out.println("比赛结束");
}
- 构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
- 每个任务执行完毕, 都调用 latch.countDown() . CountDownLatch 内部的计数器同时自减.
- 主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了.