1503 - JUC高并发

天下事情太多,不近我身,便都不是重要事。

0.思维导图

9.ReentrantReadWriteLock读写锁

9.1 悲观锁和乐观锁

悲观锁:顾名思义,它是干什么都很悲观,所以在操作的时候,每次都先上锁,使用时解锁。

乐观锁:它很乐观,多线程,并不上锁,但是会发生线程安全问题,通过比较版本号来同步。

9.2 表锁、行锁、读锁、死锁

表锁:整个表操作,不会发生死锁。

行锁:每个表中的单独一行进行加锁,会发生死锁。

读锁:共享锁(可以有多个人读),会发生死锁。

写锁:独占锁(只能有一个人写),会发生死锁。

9.3 读写锁概述

读写锁:一个资源可以被多个读线程访问,也可以被一个写线程访问,但不能同时存在读写线程,读写互斥,读读共享。

使用介绍

  • 创建读写锁对象 private ReadWriteLock rwLock = new ReentrantReadWriteLock();
  • 写锁 加锁 rwLock.writeLock().lock();,解锁为rwLock.writeLock().unlock();
  • 读锁 加锁 rwLock.readLock().lock();,解锁为rwLock.readLock().unlock();

不加读写锁的代码示例

class MyCache {
    // 模拟从Map中取对象
    private volatile Map<String, Object> map = new HashMap<>();

    // 放数据
    public void put(String key, Object value) {
        try {
            System.out.println(Thread.currentThread().getName() + "正在写操作" + key);
            // 暂停一会
            TimeUnit.MICROSECONDS.sleep(300);
            // 放数据
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写完了" + key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 取数据
    public void get(String key) {
        try {
            System.out.println(Thread.currentThread().getName() + "正在读操作" + key);
            // 暂停一会
            TimeUnit.MICROSECONDS.sleep(300);
            // 放数据
            System.out.println(map.get(key));
            System.out.println(Thread.currentThread().getName() + "取完了" + key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();

        for (int i = 0; i < 5; i++) {
            final int num = i;
            new Thread(() -> myCache.put(String.valueOf(num), String.valueOf(num)), String.valueOf(i)).start();
        }
        for (int i = 0; i < 5; i++) {
            final int num = i;
            new Thread(() -> myCache.get(String.valueOf(num)), String.valueOf(i)).start();
        }
    }
}

运行结果

线程在写操作的时候,如果有线程在读操作,可能会出现脏数据。

添加读写锁代码示例

class MyCache {
    private final Map<String, Object> map = new HashMap<>();
    //创建读写锁对象
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    // 放数据
    public void put(String key, Object value) {
        //添加写锁
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "正在写操作" + key);
            // 暂停一会
            TimeUnit.MICROSECONDS.sleep(300);
            // 放数据
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写完了" + key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放写锁
            rwLock.writeLock().unlock();
        }
    }

    // 取数据
    public void get(String key) {
        //添加读锁
        rwLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "正在读操作" + key);
            // 暂停一会
            TimeUnit.MICROSECONDS.sleep(300);
            // 放数据
            System.out.println(map.get(key));
            System.out.println(Thread.currentThread().getName() + "读完了" + key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放读锁
            rwLock.readLock().unlock();
        }
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();

        for (int i = 0; i < 5; i++) {
            final int num = i;
            new Thread(() -> myCache.put(String.valueOf(num), String.valueOf(num)), String.valueOf(i)).start();
        }
        for (int i = 0; i < 5; i++) {
            final int num = i;
            new Thread(() -> myCache.get(String.valueOf(num)), String.valueOf(i)).start();
        }
    }
}

运行结果

9.4 读写锁的演变历程

无锁独占锁读写锁

多线程抢夺资源,乱

synchronized和ReentrantLock,每次只能由一个线程操作

ReentrantReadWriteLock,读读共享,读写互斥,写写互斥。

缺点:

造成锁饥饿,一直读或者一直写。

读时候,不能写,只有读完成之后,才可以写,写操作完了之后才能读。

9.5 锁降级的过程和必要性 

写锁降级为读锁,读锁不能升级为写锁。先写才能读,提高数据可见性。

锁降级过程:获取写锁 -> 获取读锁 -> 释放写锁 -> 释放读锁。

10.阻塞队列

10.1 阻塞队列概述

阻塞队列是共享队列(多线程操作),一端输入,一端输出,不能无限放队列,满了之后就会进入阻塞,取出也同理。

  • 当队列是空的,从队列中获取元素的操作将会被阻塞。
  • 当队列是满的,从队列中添加元素的操作将会被阻塞。
  • 试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素。
  • 试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增。

10.2 阻塞队列架构

父类接口

实现类

10.3 阻塞队列分类

  • ArrayBlockingQueue(常用)

由数组结构组成的有界阻塞队列。生产者放入数据和消费者获取数据,都是共用同一个锁对象,无法并行。

  • LinkedBlockingQueue(常用)

由链表结构组成的有界阻塞队列。LinkedBlockingQueue对于生产者端和消费者端分别采用了独立的锁来控制数据同步,能够高效的处理并发数据。这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

  • DelayQueue

使用优先级队列实现的延迟无界阻塞队列。DelayQueue 中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。

  • SynchronousQueue

不存储元素的阻塞队列,也即单个元素的队列

  • LinkedTransferQueue

由链表结构组成的无界阻塞 TransferQueue 队列。

  • LinkedBlockingDeque

由链表结构组成的双向阻塞队列。

10.4 阻塞队列核心方法

11.线程池

11.1 线程池概述

连接池是创建和管理一个连接的缓冲池的技术,这些连接准备好被任何需要它们的线程使用。

线程池(英语:thread pool)一种线程使用模式。

线程池的特点: 线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超过数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。

线程池的优势

  1. 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
  2. 提高响应速度:当任务到达时,任务可以不需要等待线程创建就能立即执行。
  3. 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

11.2 线程池类图架构

Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,ExecutorService,ThreadPoolExecutor 这几个类, Executors 工具类可以协助创建线程池。

11.3 线程池使用方式 

  • Executors.newFixedThreadPool(int)

一池N线程,创建一个固定大小的线程池,可以很好的控制线程的并发量,线程可以重复使用,再显示关闭之前,都将一直存在,超出的线程会在队列中等待,在显示关闭之前线程一致存在。

  • Executors.newSingleThreadExecutor()

一池一线程,创建单个线程数的线程池,它可以保证先进先出的执行顺序。

  • Executors.newCachedThreadPool()

一池可扩容线程,创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。

方法

  • 执行线程:execute()
  • 关闭线程池:shutdown()

代码示例

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 一池五线程
//        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        // 一池单线程
//        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        // 可扩容线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();
        try {
            for (int i = 0; i < 20; i++) {
                // 执行
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "正在办理业务");
                });
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            // 关闭线程池
            threadPool.shutdown();
        }
    }
}

11.4 线程池底层原则

三种创建线程池的方法底层都是创建 ThreadPoolExecutor 这个对象。

11.5 线程池的七个参数

int corePoolSize:常驻线程数量(核心)
int maximumPoolSize:最大线程数量
long keepAliveTime:当线程数大于corePoolSize核心时,线程池中空闲线程等待时间
TimeUnit unit:线程存活时间单位
BlockingQueue workQueue:在执行任务之前用于保存任务的阻塞队列(排队的任务放入)
ThreadFactory threadFactory:线程工厂,用于创建线程
RejectedExecutionHandler handler:拒绝策略(线程满了)

11.6 线程池底层工作流程

工作流程

  1. 在创建ThreadPoolExecutor对象的时候不会创建线程,执行 execute()才会创建线程。
  2. 请求先到常驻线程(核心),满了之后再到阻塞队列进行等待,阻塞队列满了之后,在往外扩容线程。
  3. 扩容线程不能大于最大线程数。大于最大线程数和阻塞队列之和后,会执行拒绝策略。

四种基本拒绝策略

  1. AbortPolicy(默认抛异常):直接抛出RejectedExecutionException异常阻止系统正常运行。
  2. CallerRunsPolicy(谁调用找谁):“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
  3. DiscardOldestPolicy(抛弃最久执行当前):抛弃队列中等待最久的任务,然后把当前任务加入队列中,尝试再次提交当前任务。
  4. DiscardPolicy(不理不问):该策略默默地丢弃无法处理的任务,不予任何处理也不抱出异常。如果允许任务丢失,这是最好的一种策略。

11.7 自定义线程池

阿里巴巴Java开发手册

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExcutor 的方式。

Executors 返回线程池对象的弊端

底层任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。

调用ThreadPoolExecutor类,自定义参数:

代码示例

public class ThreadPoolDemo2{
    public static void main(String[] args) {
        // 组定义线程池
        ExecutorService threadPool = new ThreadPoolExecutor(
                // 常驻线程数量(核心)2个
                2,
                // 最大线程数量5个
                5,
                // 线程存活时间:2秒
                2L,
                TimeUnit.SECONDS,
                // 阻塞队列
                new ArrayBlockingQueue<>(3),
                // 默认线程工厂
                Executors.defaultThreadFactory(),
                // 拒绝策略。抛出异常
                new ThreadPoolExecutor.AbortPolicy()
        );

        try{
            for (int i = 1; i <= 8; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" 办理业务");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            // 关闭线程池
            threadPool.shutdown();
        }
    }
}

12.Fork与Join分支

分而治之

Fork:就是把一个大任务切分为若干子任务并行的执行。

Join:就是合并这些子任务的执行结果,最后得到这个大任务的结果。

【案例】使用 Fork/Join 框架计算0~100的和

流程

  1. 设置任务分割的大小。
  2. 继承 RecursiveAction 或者 RecursiveTask 重写其 compute 方法。
  3. 在 compute 方法里首先判断当前任务是否足够小,,如果如果小就执行,,如果大了就再次初始化任务去执行fork去分割。
  4. 累加返回结果,注意这一步骤是你继承了 RecursiveTask 才有的,,如果你继承的是RecursiveAction 是没有返回值的。
  5. 在主线程里使用 Future 对象去具体的运行 ForkJoin 的 submit() 方法, 得到的结果使用Future对象的 get 就可以获得。

代码示例

public class ForkJoinDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyTask myTask = new MyTask(0, 100);
        // 创建分支合并池对象
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(myTask);
        // 获取最终合并结果
        Integer res = forkJoinTask.get();
        System.out.println(res);
        // 关闭分支合并池对象
        forkJoinPool.shutdown();
    }
}

class MyTask extends RecursiveTask<Integer> {
    // 拆分时差值不超过10
    private static final Integer VALUE = 10;
    private final int begin;
    private final int end;
    private int result;

    public MyTask(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }

    //拆分和合并过程
    @Override
    protected Integer compute() {
        // 判断
        if ((end - begin) <= VALUE) {
            for (int i = begin; i <= end; i++) {
                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;
    }
}

13.异步回调

同步:指等待资源(阻塞)。

异步:指设立哨兵,资源空闲通知线程,否则该线程去做其他事情(非阻塞)。

13.1 CompletableFuture

CompletableFuture 在 Java 里面被用于异步编程,异步通常意味着非阻塞,可以使得我们的任务单独运行在与主线程分离的其他线程中,并且通过回调可以在主线程中得到异步任务的执行状态,是否完成,和是否异常等信息。

CompletableFuture 实现了 Future, CompletionStage 接口,实现了 Future接口就可以兼容现在有线程池框架,而 CompletionStage 接口才是异步编程的接口抽象,里面定义多种异步方法,其中异步调用没有返回值方法runAsync,异步调用有返回值方法supplyAsync

public class CompletableFutureDemo {
       public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 异步调用,没返回值
        CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "future1");
        });
        future1.get();

        // 异步调用,有返回值
        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "future2");
            //模拟异常
            int i = 1 / 0;
            return 1024;
        });
        future2.whenComplete((t, u) -> {
            System.out.println("t=" + t);//t是返回值
            System.out.println("u=" + u);//u是异常
        }).get();
    }
}

运行结果

  • 54
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值