文章目录
- 一:Callable接口
- 二:ReentrantLock
- 三:原子类
- 四:信号量Semaphore
JUC:JUC是java.util.concurrent
包的简称,目的就是为了更好的支持高并发任务。让开发者进行多线程编程时减少竞争条件和死锁的问题
一:Callable接口
Callable接口:Callable
接口类似于Runnable
,也即它的实例也可以像Runnable
一样传给线程去执行,但是Runnable
不返回结果,也不会抛出受查异常,而Callable
与此相反
下面使用"1 + 2 + 3 +…+ 1000"这样一个例子来展示使用Callable
和不使用Callable
时代码的区别
①不使用Callable
: 可以看到,这种实现逻辑需要借助一个辅助类,还需要使用一系列加锁、wait
等操作,比较繁琐。具体实现方式如下(不唯一)
- 创建一个类
Result
,包含一个sum
表示最终结果,一个lock
表示线程锁对象 main
方法内先创建Result
实例,然后创建一个线程thread
,在线程内部计算"1 + 2 + 3 +…+ 1000"- 主线程同时使用
wait
等待线程thread
计算结束 - 当线程
thread
计算完毕之后,通过notify
唤醒主线程,接着主线程打印结果
public class TestDemo {
static class Result{
public int sum = 0;
public final Object lock = new Object();
}
public static void main(String[] args) throws InterruptedException {
Result result = new Result();
Thread thread = 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();
}
}
};
thread.start();
synchronized (result.lock){
while(result.sum == 0){
result.lock.wait();
}
}
System.out.println("sum:" + result.sum);
}
}
②使用Callable
: 可以看到,在使用Callable
和FutureTask
之后,代码简化了许多,并且不用手动写线程同步代码了。具体实现方式如下
- 创建一个匿名内部类实现
Callable
接口。Callable
带有泛型参数,它表示返回值的类型 - 重写
Callable
中的call
方法,完成计算并直接通过return
返回计算结果 - 由于
Thread
的构造方法中不能直接传入Callable
,所以还需要用FutureTask
把Callable
的实例给包装一下 - 创建线程,在其构造方法中传入
FutureTask
。此时,线程就会执行FutureTask
内部的Callable
的call
方法,完成计算后结果就被放入到了FutureTask
对象中 - 在
main
方法中调用futureTask.get()
后就会阻塞等到线程计算完毕,并获取到FutureTask
中的结果
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class TestDemo2 {
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;
}
};
// Thread构造方法不能直接传入Callable,所以需要借助一个中间类
FutureTask<Integer> futureTask = new FutureTask<>(callable);
// 创建线程执行任务
Thread thread = new Thread(futureTask);
thread.start();
// 获取线程计算结果
// get方法会阻塞,直到call方法计算完毕
System.out.println(futureTask.get());
}
}
二:ReentrantLock
ReentrantLock:和Synchronized
类似,也是可重入锁,用来实现互斥效果,保证线程安全,相较于Synchronized
来说更加灵活,也具有更多的方法。具体来说ReentrantLock
有三个用法
lock()
:加锁(如果获取不到就会死等)trylock(超时时间)
:加锁(如果获取不到锁,在等待一段时间后就会放弃加锁)unlock()
:解锁
ReentrantLock lock = new ReentrantLock();
lock.lock();
try{
//working....
}finally{
lock.unlock(); // 千万不要忘记解锁
}
ReentrantLock
和Synchronized
区别如下
Synchronized
是一个关键字,是JVM内部实现的;ReentrantLock
是标准库的一类,是在JVM外实现的Synchronized
使用时不需要手动释放锁;ReentrantLock
使用时需要手动释放,使用起来更加灵活,但也容易忘记解锁Synchronized
在申请锁失败时会死等;ReentrantLock
可以通过trylock
的方式等待一段时间就放弃Synchronized
是非公平锁,ReentrantLock
默认为非公平锁,可以通过构造方法传入一个true
开启公平锁模式ReentrantLock
具有更加强大的唤醒机制Synchronized
是通过Object
的wait/notify
来实现的,每次随机唤醒一个等待的线程ReentrantLock
搭配Condition
类实现,可以精确控制唤醒某个指定的线程
ReentrantLock
和Synchronized
如何选择
- 锁竞争不激烈的时候使用
Synchronized
:效率会更高,自动释放也方便 - 锁竞争激烈的时候使用
ReentrantLock
:可以搭配trylock更灵活地控制加锁行为,而不至于死等 - 如果需要公平锁则使用
ReentrantLock
三:原子类
原子类:原子类内部通过CAS实现(前文已讲过),所以性能要比加锁实现i++
高很多,主要有以下几个
AtomicBoolean
AtomicInteger
AtomicIntegerArray
AtomicLong
AtomicReference
AtomicStampedReference
以AtomicBoolean
为例,涉及方法有
addAndGet(int delta)
:i += delta
decrementAndGet()
:--i
getAndDecrement()
:i--
incrementAndGet()
:++i
getAndIncrement()
:i++
public class TestDemo3 {
public static void main(String[] args) throws InterruptedException {
AtomicInteger count = new AtomicInteger(0);
Thread thread1= new Thread(){
@Override
public void run(){
for(int i = 0; i < 50000; i++){
// 相当于count++
count.getAndIncrement();
}
}
};
Thread thread2= new Thread(){
@Override
public void run(){
for(int i = 0; i < 50000; i++){
// 相当于count++
count.getAndIncrement();
}
}
};
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("count:" + count.get());
}
}
四:信号量Semaphore
- 信号量属于操作系统重点内容,这里只做简单介绍
信号量:本质就是一个变量(分为整形和记录型两种),表示系统中某种资源的数量。控制信号量有两种原子操作:
- P操作(wait(S)原语):这个操作会把信号量减去1,相减后如果信号量<0则表示资源已经被占用,进程需要阻塞;相减后如果信号量 ≥ 0 \ge0 ≥0,则表明还有资源可以使用,进程可以正常执行
- V操作(signal(S)原语):这个操作会把信号量加上1,相加后如果信号量 ≤ 0 \le0 ≤0,则表明当前有阻塞中的进程,于是会把该进程唤醒;相加后如果信号量>0,则表明当前没有阻塞中的进程