
🔥个人主页: 寻星探路
🎬作者简介:Java研发方向学习者
📖个人专栏:、《
⭐️人生格言:没有人生来就会编程,但我生来倔强!!!
目录
[一、Callable 接口](#一、Callable 接口)
[2.1ExecutorService 和 Executors](#2.1ExecutorService 和 Executors)
一、Callable 接口
Callable 是一个interface,相当于把线程封装了一个"返回值",方便程序猿借助多线程的方式计算结果。
代码示例:创建线程计算1+2+3+...+1000,不使用Callable版本
• 创建一个类Result,包含一个sum表示最终结果,lock表示线程同步使用的锁对象。
• main方法中先创建Result实例,然后创建一个线程 t ,在线程内部计算1+2+3+...+1000。
• 主线程同时使用wait等待线程 t 计算结束。(注意,如果执行到wait之前,线程 t 已经计算完了,就不必等待了)。
• 当线程 t 计算完毕后,通过notify唤醒主线程,主线程再打印结果。
java
static class Result {
public int sum = 0;
public Object lock = new Object();
}
public static void main(String[] args) throws InterruptedException {
Result result = new Result();
Thread t = new Thread() {
@Override
public void run() {
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
synchronized (result.lock) {
result.sum = sum;
result.lock.notify();
}
}
};
t.start();
synchronized (result.lock) {
while (result.sum == 0) {
result.lock.wait();
}
System.out.println(result.sum);
}
}

可以看到,上述代码需要一个辅助类Result,还需要使用一系列的加锁和waitnotify操作,代码复杂,容易出错。
代码示例:创建线程计算1+2+3+...+1000,使用Callable版本
• 创建一个匿名内部类,实现Callable接口,Callable带有泛型参数,泛型参数表示返回值的类型。
• 重写Callable的call方法,完成累加的过程,直接通过返回值返回计算结果。
• 把callable实例使用FutureTask包装一下。
• 创建线程,线程的构造方法传入FutureTask,此时新线程就会执行FutureTask内部的Callable的 call 方法,完成计算,计算结果就放到了FutureTask对象中。
• 在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕,并获取到FutureTask中的结果。
java
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Main {
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 = 1; i <= 1000; i++) {
sum += i;
}
return sum;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
int result = futureTask.get();
System.out.println(result);
}
}

可以看到,使用Callable和FutureTask之后,代码简化了很多,也不必手动写线程同步代码了。
理解Callable
Callable 和 Runnable相对,都是描述一个"任务",Callable描述的是带有返回值的任务,Runnable 描述的是不带返回值的任务。
Callable 通常需要搭配FutureTask来使用,FutureTask用来保存Callable的返回结果,因为Callable 往往是在另一个线程中执行的,啥时候执行完并不确定。
FutureTask 就可以负责这个等待结果出来的⼯作。
理解FutureTask
想象去吃麻辣烫,当餐点好后,后厨就开始做了。同时前台会给你一张"小票",这个小票就是 FutureTask,后⾯我们可以随时凭这张小票去查看自己的这份麻辣烫做出来了没。
二、ReentrantLock
可重⼊互斥锁和synchronized定位类似,都是用来实现互斥效果,保证线程安全。
ReentrantLock 也是可重入锁,Reentrant这个单词的原意就是"可重入"。
ReentrantLock 的用法:
• lock():加锁,如果获取不到锁就死等。
• trylock(超时时间):加锁,如果获取不到锁,等待一定的时间之后就放弃加锁。
• unlock():解锁。
java
ReentrantLock lock = new ReentrantLock();
----------------------------------------
lock.lock();
try {
// working
} finally {
lock.unlock()
}
ReentrantLock 和 synchronized 的区别:
• synchronized是一个关键字,是JVM内部实现的(大概率是基于C++实现),ReentrantLock是标准 库的一个类,在JVM外实现的(基于Java实现)。
• synchronized使用时不需要手动释放锁,ReentrantLock使用时需要手动释放,使用起来更灵活,但是也容易遗漏unlock。
• synchronized在申请锁失败时,会死等,ReentrantLock可以通过trylock的方式等待一段时间就放 弃。
• synchronized是非公平锁,ReentrantLock默认是非公平锁,可以通过构造方法传入一个true开启 公平锁模式。
java
// ReentrantLock 的构造⽅法
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
• 更强大的唤醒机制,synchronized是通过Object的wait/notify实现等待---唤醒,每次唤醒的是一个 随机等待的线程,ReentrantLock搭配Condition类实现等待---唤醒,可以更精确控制唤醒某个指定 的线程。
如何选择使用哪个锁?
• 锁竞争不激烈的时候,使用synchronized,效率更高,自动释放更方便。
• 锁竞争激烈的时候,使用ReentrantLock,搭配trylock更灵活控制加锁的行为,而不是死等。
• 如果需要使用公平锁,使用ReentrantLock。
1、原子类
原子类内部用的是CAS实现,所以性能要比加锁实现i++高很多。原子类有以下几个
• AtomicBoolean
• AtomicInteger
• AtomicIntegerArray
• AtomicLong
• AtomicReference
• AtomicStampedReference
以AtomicInteger 举例,常见方法有
java
addAndGet(int delta); i += delta;
decrementAndGet(); --i;
getAndDecrement(); i--;
incrementAndGet(); ++i;
getAndIncrement(); i++;
2、线程池
虽然创建销毁线程比创建销毁进程更轻量,但是在频繁创建销毁线程的时候还是会比较低效。
线程池就是为了解决这个问题,如果某个线程不再使用了,并不是真正把线程释放,而是放到一个"池子"中,下次如果需要用到线程就直接从池子中取,不必通过系统来创建了。
2.1ExecutorService 和 Executors
代码示例:
• ExecutorService 表示一个线程池实例。
• Executors是一个工厂类,能够创建出几种不同风格的线程池。
• ExecutorService 的 submit 方法能够向线程池中提交若干个任务。
java
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
Executors 创建线程池的几种方式:
• newFixedThreadPool:创建固定线程数的线程池。
• newCachedThreadPool:创建线程数目动态增长的线程池。
• newSingleThreadExecutor:创建只包含单个线程的线程池。
• newScheduledThreadPool:设定延迟时间后执行命令,或者定期执行命令,是进阶版的Timer。
Executors 本质上是ThreadPoolExecutor类的封装。
2.2ThreadPoolExecutor
ThreadPoolExecutor 提供了更多的可选参数,可以进一步细化线程池行为的设定。
ThreadPoolExecutor 的构造方法

**理解ThreadPoolExecutor构造方法的参数**
把创建一个线程池想象成开个公司,每个员工相当于一个线程。
• corePoolSize:正式员工的数量。(正式员工,一旦录用,永不辞退)
• maximumPoolSize:正式员工 + 临时工的数目。(临时工:一段时间不干活,就被辞退)
• keepAliveTime:临时工允许的空闲时间。
• unit:keepaliveTime 的时间单位、是秒、分钟,还是其他值。
• workQueue:传递任务的阻塞队列。
• threadFactory:创建线程的工厂,参与具体的创建线程工作。
• RejectedExecutionHandler:拒绝策略,如果任务量超出公司的负荷了接下来怎么处理。
◦ AbortPolicy():超过负荷,直接抛出异常。
◦ CallerRunsPolicy():调用者负责处理。
◦ DiscardOldestPolicy():丢弃队列中最老的任务。
◦ DiscardPolicy():丢弃新来的任务。
代码示例:
java
ExecutorService pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>(),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy
for(int i=0;i<3;i++) {
pool.submit(new Runnable() {
@Override
void run() {
System.out.println("hello");
}
});
}
2.3线程池的工作流程

3、信号量Semaphore
信号量,用来表示"可用资源的个数",本质上就是一个计数器。
理解信号量
可以把信号量想象成是停车场的展示牌:当前有车位100个,表示有100个可用资源。
当有车开进去的时候,就相当于申请一个可用资源,可用车位就-1(这个称为信号量的P操作)
当有车开出来的时候,就相当于释放一个可用资源,可用车位就+1(这个称为信号量的V操作)
如果计数器的值已经为0了,还尝试申请资源,就会阻塞等待,直到有其他线程释放资源。
Semaphore的PV操作中的加减计数器操作都是原子的,可以在多线程环境下直接使用。
代码示例
• 创建Semaphore示例,初始化为4,表示有4个可用资源。
• acquire方法表示申请资源(P操作),release方法表示释放资源(V操作)。
• 创建20个线程,每个线程都尝试申请资源,sleep1秒之后,释放资源,观察程序的执行效果。
java
Semaphore semaphore = new Semaphore(4);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println("申请资源");
semaphore.acquire();
System.out.println("我获取到资源了");
Thread.sleep(1000);
System.out.println("我释放资源了");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 20; i++) {
Thread t = new Thread(runnable);
t.start();
}
4、CountDownLatch
同时等待N个任务执行结束。
好像跑步比赛,10个选⼿依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。
• 构造CountDownLatch实例,初始化10表示有10个任务需要完成。
• 每个任务执行完毕,都调用 latch.countDown() 。在CountDownLatch内部的计数器同时自减。
• 主线程中使用 latch.await();阻塞等待所有任务执行完毕,相当于计数器为0了。
java
public class Demo {
public static void main(String[] args) throws Exception {
CountDownLatch latch = new CountDownLatch(10);
Runnable r = new Runable() {
@Override
public void run() {
try {
Thread.sleep(Math.random() * 10000);
latch.countDown();
} catch (Exception e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 10; i++) {
new Thread(r).start();
}
// 必须等到 10 ⼈全部回来
latch.await();
System.out.println("⽐赛结束");
}
}
5、相关面试题
1)线程同步的方式有哪些?
synchronized,ReentrantLock,Semaphore 等都可以用于线程同步。
2)为什么有了 synchronized 还需要 juc 下的 lock ?
以juc的ReentrantLock为例,
• synchronized使用时不需要手动释放锁,ReentrantLock使用时需要手动释放,使用起来更灵活。
• synchronized在申请锁失败时,会死等。ReentrantLock可以通过trylock的方式等待一段时间就放弃。
• synchronized是非公平锁,ReentrantLock默认是非公平锁。可以通过构造方法传入一个true开启公平锁模式。
• synchronized 是通过 Object 的 wait / notify 实现等待---唤醒,每次唤醒的是一个随机等待的线程。
ReentrantLock 搭配Condition类实现等待---唤醒,可以更精确控制唤醒某个指定的线程。
3)AtomicInteger 的实现原理是什么?
基于CAS机制,伪代码如下:
javaclass AtomicInteger { private int value; public int getAndIncrement() { int oldValue = value; while ( CAS(value, oldValue, oldValue+1) != true) { oldValue = value; } return oldValue; } }
4)信号量听说过么?之前都用在过哪些场景下?
信号量,用来表示"可用资源的个数",本质上就是一个计数器。
使用信号量可以实现"共享锁",比如某个资源允许3个线程同时使用,那么就可以使用P操作作为加锁,V操作作为解锁,前三个线程的P操作都能顺利返回,后续线程再进行P操作就会阻塞等待,直到前面的线程执行了V操作。