Java多线程常见面试题-第二节:JUC(java.util.concurrent)
Posted 快乐江湖
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程常见面试题-第二节:JUC(java.util.concurrent)相关的知识,希望对你有一定的参考价值。
文章目录
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
- 信号量属于操作系统重点内容,这里只做简单介绍
- 原始系统级别的信号量使用见:Linux系统编程40:多线程之基于环形队列的生产者与消费者模型
(1)什么是信号量
信号量:本质就是一个变量(分为整形和记录型两种),表示系统中某种资源的数量。控制信号量有两种原子操作:
- P操作(wait(S)原语):这个操作会把信号量减去1,相减后如果信号量<0则表示资源已经被占用,进程需要阻塞;相减后如果信号量 ≥ 0 \\ge0 ≥0,则表明还有资源可以使用,进程可以正常执行
- V操作(signal(S)原语):这个操作会把信号量加上1,相加后如果信号量 ≤ 0 \\le0 ≤0,则表明当前有阻塞中的进程,于是会把该进程唤醒;相加后如果信号量>0,则表明当前没有阻塞中的进程
(2)java.util.concurrent.Semaphore
java.util.concurrent.Semaphore:Java将信号量有关的系统接口进行封装,也即java.util.concurrent.Semaphore
中,其中
seamphore.acuire()
对应的是P操作seamphore.release()
对应的是V操作
如下申请4个资源,然后连续进行4次P操作后程序阻塞
import java.util.concurrent.Semaphore;
public class TestDemo4
public static void main(String[] args) throws InterruptedException
// 4个可用资源
Semaphore semaphore = new Semaphore(4);
// 连续申请4个资源
semaphore.acquire();
System.out.println("P操作");
semaphore.acquire();
System.out.println("P操作");
semaphore.acquire();
System.out.println("P操作");
semaphore.acquire();
System.out.println("P操作");
// 此时资源数目已空, 申请操作将被阻塞
semaphore.acquire();
System.out.println("P操作");
semaphore.release();
System.out.println("V操作");
五:CountDownLatch
CountDownLatch:用于同时等待N个任务执行结束。具体来说,每个任务执行完毕后,都会调用latch.countDown()
,然后CountDownLatch
内部的计数器就会减一,主线程中会使用latch.await()
,用于阻塞等待所有任务执行完毕(此时计数器为0)
import java.util.concurrent.CountDownLatch;
public class TestDemo5
public static void main(String[] args) throws InterruptedException
// 10个任务
CountDownLatch countDownLatch = new CountDownLatch(10);
for(int i = 0; i < 10; i++)
//创建10个线程分别执行这10个任务
Thread thread = new Thread()
@Override
public void run()
System.out.println("任务开始" + Thread.currentThread().getName());
try
Thread.sleep(1000);
catch (InterruptedException e)
throw new RuntimeException(e);
System.out.println("任务结束" + Thread.currentThread().getName());
// 任务完成
countDownLatch.countDown();
;
thread.start();
// 阻塞等待所有线程执行任务完毕
countDownLatch.await();
System.out.println("结束!");
以上是关于Java多线程常见面试题-第二节:JUC(java.util.concurrent)的主要内容,如果未能解决你的问题,请参考以下文章