JUC学习笔记(二)

目录

9.死锁

9.1什么是死锁

9.2产生死锁的原因

9.3死锁案例

9.4 验证是否死锁

10 Callable接口

10.1 Callable接口的特点(*)

10.2 Callable与Runnable接口区别

10.3 创建Callable线程

10.4 FutureTask原理(未来任务)

11 JUC强大的辅助类

11.1CountDownLatch(减少计数)

11.2 场景

11.3 CyclicBarrier(循环栅栏)

11.3 场景

11.4 Semaphore(信号灯)

11.5 场景

12 读写锁

12.1概述

12.2 读写锁案例

12.3 读写锁的演变

12.4 读写锁的降级

13.阻塞队列

13.1 BlockingQueue简介

13.2 阻塞队列分类

13.3 BlockingQueue核心方法

14.ThreadPool线程池

14.1 线程池概述

14.2 线程池的使用方式

14.3 线程池底层

14.4 线程池的七个参数

14.5 线程池底层工作流程

14.6 自定义线程池

15.Fork/Join分支合并框架

16.JUC异步回调


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();
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值