目录
9.死锁
9.1什么是死锁
两个或两个以上的进程在执行过程中,因为争夺资源而造成一种互相等待的现象,如果没有外力干涉,他们无法再执行下去。
9.2产生死锁的原因
(1)系统资源不足
(2)进程运行推进顺序不合适
(3)资源分配不当
9.3死锁案例
创建两个线程,在第一个线程中获取第二个线程的锁,在第二个线程中获取第一个线程的锁。
public class DeadLock {
public static void main(String[] args) throws InterruptedException {
Object a=new Object();
Object b=new Object();
new Thread(()->{
synchronized (a){
System.out.println(Thread.currentThread().getName()+":锁a试图获取锁b");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b){
System.out.println("获取锁b");
}
}
},"a").start();
new Thread(()->{
synchronized (b){
System.out.println(Thread.currentThread().getName()+":锁b试图获取锁a");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a){
System.out.println("获取锁a");
}
}
},"b").start();
}
}
9.4 验证是否死锁
(1)jps 类似于linux ps -ef
(2)jstack
首先在ideal的控制台输入jps -l;找到对应的id号,使用jstack id 查看是否死锁。
10 Callable接口
目前我们学习了两种创建线程的方法:一种是通过创建Thread类,另一种是通过使用Runnable创建线程。但是,Runnable缺少的一项功能是,当线程终止时(run()执行完毕时),我们无法使线程返回结果。为了支持此功能,Java中提供了Callable接口。
10.1 Callable接口的特点(*)
-
为了实现Runnable,需要实现不返回任何内容的run()方法,而对于Callable,需要实现在完成时返回结果的call()方法。
-
call()方法可以引发异常,而run()方法不能。
-
为实现Callable而必须重写call方法。
-
不能直接替换Thread构造方法中的Runnable,因为Thread类的构造方法根本没有Callable。
10.2 Callable与Runnable接口区别
-
是否有返回值
-
是否抛出异常
-
实现方法名称不同,一个是run方法,一个是call方法。
10.3 创建Callable线程
需要使用到Runnable接口的一个实现类FutureTask。用来建立Callable与Runnable的联系。
FutureTask的构造函数中可以传递Callable。
class MyThread2 implements Callable{
@Override
public Integer call() throws Exception {
return 200;
}
}
public class Test1 {
public static void main(String[] args) {
//Callable接口创建线程
FutureTask<Integer> futureTask=new FutureTask<>(new myThread2());
new Thread(futureTask,"BB").start();
}
}
10.4 FutureTask原理(未来任务)
场景引入:
(1)老师上课,口渴了,去买水不合适,讲课线程需要继续。单独开启一个线程让班长帮我买水,把水买回来需要的时候直接get。在不影响主线程的情况下开启另外一个线程完成另外一个任务。
(2)4个同学,1同学1+2+...+5, 2同学10+11+12+...+50,3同学60+61+62,4同学100+200,第二个同学计算量比较大。当老师询问计算结果时,由于2同学计算量较大,老师会先询问下一个同学,给2同学单独开一个线程,让他继续计算,最后再来询问结果。
(3)考试的时候先做会做的题目,最后做不会的。
只汇总一次。
案例:
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask futureTask = new FutureTask(() -> {
return 200;
});
new Thread(futureTask,"futureTask").start();
while (!futureTask.isDone()){
System.out.println("wait.........");
}
System.out.println(Thread.currentThread().getName()+":"+futureTask.get());
//System.out.println(Thread.currentThread().getName()+":"+futureTask.get());
System.out.println(Thread.currentThread().getName()+"over");
}
}
运行结果:
wait.........
wait.........
wait.........
main:200
mainover
将上述代码的注释打开,打印结果:
wait.........
wait.........
wait.........
main:200
main:200
mainoverCallable只会汇总一次;
11 JUC强大的辅助类
11.1CountDownLatch(减少计数)
CountDownLatch类可以设置一个计数器,然后通过countDown方法来进行减1的操作,使用await方法等待计数器不大于0,然后继续执行await方法之后的语句。
-
CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
-
其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)。
-
当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。
11.2 场景
6个同学陆续离开教师后,班长才能锁门。
public class Test {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch=new CountDownLatch(6);
for (int i = 1; i <=6 ; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"号同学走人了");
countDownLatch.countDown();
},String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println("锁门");
}
}
当 CountDownLatch的值没有减为0时,主线程会一直阻塞,直到为0是主线程才会继续执行。
11.3 CyclicBarrier(循环栅栏)
一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点(common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时CyclicBarrier很有用。因为该Barrier在释放等待线程后可以重用,所以称它为循环的barrier。
CyclicBarrier支持一个可选的Runnable命令,在一组线程中的最后一个线程到达之后(但在释放所有线程之前),该命令只在每个屏障点运行一次。若在继续所有参与线程之前更新共享状态,此屏障操作很有用。
CyclicBarrier看英文单词可以看出大概就是循环阻塞的意思,在使用中CyclicBarrier的构造方法第一个参数是目标障碍数,每次执行cyclicBarrier一次障碍数会加一,如果达到了目标障碍数,才会执行cyclicBarrier.await()之后的语句。可以将CyclicBarrier理解为加1操作。
11.3 场景
集齐7颗龙珠可以召唤神龙,每个人找一个龙珠
public class Test {
public static int NUM=7;
public static void main(String[] args) {
CyclicBarrier cyclicBarrier=new CyclicBarrier(NUM,()->{
System.out.println("集齐7颗大宝贝,召唤神虫");
});
for (int i = 1; i <=7 ; i++) {
new Thread(()->{
System.out.println("第"+Thread.currentThread().getName()+"个大宝贝");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
11.4 Semaphore(信号灯)
一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个acquire(),然后再获取该许可。每个release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采取相应行动。通常用于限制可以访问某些资源的线程数目。
11.5 场景
6部汽车,三个停车位。
public class Test {
public static void main(String[] args) {
Semaphore semaphore=new Semaphore(3);
for (int i = 1; i <=6; i++) {
new Thread(()->{
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+" 号车抢到了车位");
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName()+" 号车离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
信号灯就好比一个萝卜一个坑,坑被占完了其它萝卜就只能等着了。
12 读写锁
12.1概述
读锁:共享锁。发生死锁。写锁:独占锁。发生死锁。读锁发生死锁原因:有两个线程,同时进行读和写,1线程读的时候,要等待2线程写操作完毕。2线程读的时候也要等待1线程写操作完毕。ps:这里应该是嵌套锁。
写锁发生死锁原因:两个线程同时写两条记录。ps:这里应该是写锁嵌套写锁。
读写锁:一个资源可以被多个读线程访问,或者可以被一个写线程访问,但是不能同时存在读写线程,读写互斥,读读共享。(读写锁的机制)
这里说明一下,既然读不会改变数据,而且读锁是共享的,那么读锁的存在是为什么?保证读的原子性;
12.2 读写锁案例
使用Map模拟缓存,5个线程读数据,5个线程写数据。
class MyCache{
private Map<String,Object> map=new HashMap<>();
private ReadWriteLock rwLock=new ReentrantReadWriteLock();
public void put(String key,Object value) throws InterruptedException {
rwLock.writeLock().lock();
System.out.println(Thread.currentThread().getName()+"正在写");
TimeUnit.SECONDS.sleep(1);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"写完了");
rwLock.writeLock().unlock();
}
public void get(String key) {
try{
rwLock.readLock().lock();
System.out.println(Thread.currentThread().getName()+"正在读");
TimeUnit.SECONDS.sleep(3);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"读取结果"+o.toString());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyCache myCache=new MyCache();
for (int i = 1; i <=5 ; i++) {
final String num=String.valueOf(i);
new Thread(()->{
try {
myCache.put(num,num);
} catch (InterruptedException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
TimeUnit.SECONDS.sleep(1);
for (int i = 1; i <=5; i++) {
final String num=String.valueOf(i);
new Thread(()->{
try {
myCache.get(num);
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
12.3 读写锁的演变
第一阶段:无锁;
第二阶段:使用synchronized或者ReentrantLock,都是独占的,每次只能来一个操作。读读不能共享。
第三阶段:ReentrantReadWriteLock 读读可以共享,提示性能,可以多人读同时操作。 缺点:(1)造成锁饥饿,一直读,没有写操作;
(2)读时候,不能写,只有读完成之后,才可以写,写操作可以读。 (指的是嵌套锁,不是指多个线程)
12.4 读写锁的降级
锁降级:将写入锁降级为读锁。
代码演示:
public class Test {
public static void main(String[] args) {
ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
writeLock.lock();
System.out.println("---write");
readLock.lock();
System.out.println("---read");
readLock.unlock();
writeLock.unlock();
}
}
写锁可以降级为读锁,但是读锁不能升级为写锁。
13.阻塞队列
13.1 BlockingQueue简介
Concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。
阻塞队列,顾名思义,首先它是一个队列,通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出。
当队列是空的,从队列中获取元素的操作将会被阻塞,直到其他线程往空队列插入新的元素。
当队列是满的,试图向队列添加元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增。
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起。
使用阻塞队列的好处:我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。
13.2 阻塞队列分类
BlockingQueue是一个接口其子类有很多;
(1)ArrayBlockingQueue:由数组结构组成的有界阻塞队列。(***)
(2)LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。(***)
(3)DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
(4)PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
(5)SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
(6)LinkedTransferQueue:由链表组成的无界阻塞队列。
(7)LinkedBlockingDequeue:由链表组成的双向阻塞队列。
13.3 BlockingQueue核心方法
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove() | poll() | take() | poll(time,unit) |
检查 | element() | peek() | 不可用 | 不可用 |
14.ThreadPool线程池
14.1 线程池概述
线程池:一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁的代价。线程池不仅能保证内核的充分利用,还能防止过分调度。
线程池的优势:线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
主要特点:
-
降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
-
提高响应速度:当任务到达时,任务可以不需要等待线程创建就能执行。
-
提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调度和监控。
-
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类。
14.2 线程池的使用方式
(1)Executors.newFixedThreadPool(int) 一池N线程
(2)Executors.newSingleThreadExecutor() 一个任务一个任务的执行,一池一线程
(3)Executors.newCachedThreadPool() 线程池根据需求创建线程,可扩容,遇强则强
public class ThreadPoolFix {
public static void main(String[] args) {
//一池5线程
ExecutorService threadPool1= Executors.newFixedThreadPool(5);
//一池一线程
ExecutorService executorService = Executors.newSingleThreadExecutor();
//一池可扩容线程
ExecutorService executorService1 = Executors.newCachedThreadPool();
try{
for (int i = 1; i <=20; i++) {
executorService1.execute(()->{
System.out.println(Thread.currentThread().getName()+" 办理业务");
});
}
}finally {
executorService1.shutdown();
}
}
}
14.3 线程池底层
上述的三个方法都返回了一个ThreadPoolExecutor类;
14.4 线程池的七个参数
public ThreadPoolExecutor(int corePoolSize,//常驻线程数量
int maximumPoolSize,//最大线程数量
long keepAliveTime,//存活时间
TimeUnit unit,//存活时间单位
BlockingQueue<Runnable> workQueue,//阻塞队列
ThreadFactory threadFactory,//线程工厂
RejectedExecutionHandler handler)//拒绝策略
14.5 线程池底层工作流程
在使用线程池调用execute()后会调用线程池的常驻线程来执行任务,当常驻线程满后,会将其余的任务放到阻塞队列,当阻塞队列满后,会开启额外的线程来处理新来的任务,当线程池的最大线程达到最大值时,再有任务过来则会执行拒绝策略。
拒绝策略:
(1)AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行。
(2)CallerRunsPolicy:调用者运行一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
(3)DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入到队列中,尝试再次提交当前任务。
(4)DiscardPolicy:该策略默默地丢弃无法处理的任务,不予以任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。
14.6 自定义线程池
public class MyThreadPool {
public static void main(String[] args) {
ExecutorService executorService = new ThreadPoolExecutor(
2, 5, 2L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()
);
try{
for (int i = 1; i <=8; i++) {
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+" 办理业务");
});
}
}finally {
executorService.shutdown();
}
}
}
15.Fork/Join分支合并框架
Fork/Join它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。Fork/Join框架要完成两件事情:
-
Fork:把一个复杂任务进行分拆,大事化小。
-
Join:把分拆任务的结果进行合并。
案例:1+2+3+...+100;
让相加的两个数,差值不能超过10
class MyTask extends RecursiveTask<Integer>{
//拆分的时候差值不能超过10;计算10以内的运算
public static final Integer VALUE=10;
private int begin;
private int end;
private int result;
public MyTask(int begin, int end) {
this.begin = begin;
this.end = end;
}
@Override
protected Integer compute() {
//判断相加的两个数值是否大于10
if(end-begin<=VALUE)
{
//相加
for (int i = begin; i <=end ; i++) {
result=result+i;
}
}
else {
//拆分
//获取中间值
int mid=(begin+end)/2;
//拆分左边
MyTask myTask1=new MyTask(begin,mid);
//拆分右边
MyTask myTask2=new MyTask(mid+1,end);
myTask1.fork();
myTask2.fork();
//合并结束
result=myTask1.join()+myTask2.join();
}
return result;
}
}
public class ForkJoin {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建拆分任务对象
MyTask myTask=new MyTask(0,100);
//创建分支合并池对象
ForkJoinPool forkJoinPool=new ForkJoinPool();
ForkJoinTask<Integer> submit = forkJoinPool.submit(myTask);
System.out.println(submit.get());
forkJoinPool.shutdown();
}
}
16.JUC异步回调
使用CompletableFuture类来完成;
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//没有返回值的异步回调
CompletableFuture<Void> completableFuture1=CompletableFuture.runAsync(()->{
System.out.println(Thread.currentThread().getName()+"completableFuture1");
});
completableFuture1.get();
//有返回值的异步调用
CompletableFuture<Integer> completableFuture2=CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()+"completableFuture2");
return 1024;
});
completableFuture2.whenComplete((t,u)->{
System.out.println("-------t="+t);//方法的返回值
System.out.println("-------u="+u);//异常信息
});
completableFuture2.get();
}
}