尚硅谷JUC高并发编程学习笔记(5)

一、读写锁

回顾悲观锁和乐观锁的概念
悲观锁:单独每个人完成事情的时候,执行上锁解锁。解决并发中的问题,不支持并发操作,只能一个一个操作,效率低 。
乐观锁:每执行一件事情,都会比较数据版本号,看谁先提交版本号。


新概念
表锁:整个表操作,不会发生死锁
行锁:每个表中的单独一行进行加锁,会发生死锁
读锁:共享锁(可以有多个人读),会发生死锁
写锁:独占锁(只能有一个人写),会发生死锁

在这里插入图片描述

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

读写锁ReentrantReadWriteLock
读锁为ReentrantReadWriteLock.ReadLockreadLock()方法。
写锁为ReentrantReadWriteLock.WriteLockwriteLock()方法。

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

案例分析:
模拟多线程在map中取数据和读数据
完整代码如下

//资源类
class MyCache {
    //创建map集合
    private volatile Map<String,Object> map = new HashMap<>();

    //创建读写锁对象
    private 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 Object get(String key) {
        //添加读锁
        rwLock.readLock().lock();
        Object result = null;
        try {
            System.out.println(Thread.currentThread().getName()+" 正在读取操作"+key);
            //暂停一会
            TimeUnit.MICROSECONDS.sleep(300);
            result = map.get(key);
            System.out.println(Thread.currentThread().getName()+" 取完了"+key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放读锁
            rwLock.readLock().unlock();
        }
        return result;
    }
}

public class ReadWriteLockDemo {
    public static void main(String[] args) throws InterruptedException {
        MyCache myCache = new MyCache();
        //创建线程放数据
        for (int i = 1; i <=5; i++) {
            final int num = i;
            new Thread(()->{
                myCache.put(num+"",num+"");
            },String.valueOf(i)).start();
        }

        TimeUnit.MICROSECONDS.sleep(300);

        //创建线程取数据
        for (int i = 1; i <=5; i++) {
            final int num = i;
            new Thread(()->{
                myCache.get(num+"");
            },String.valueOf(i)).start();
        }
    }
}

执行结果

2 正在写操作2
2 写完了2
1 正在写操作1
1 写完了1
3 正在写操作3
3 写完了3
4 正在写操作4
4 写完了4
5 正在写操作5
5 写完了5
1 正在读取操作1
2 正在读取操作2
3 正在读取操作3
5 正在读取操作5
4 正在读取操作4
5 取完了5
2 取完了2
4 取完了4
3 取完了3
1 取完了1

总结锁的演变

1.  无锁:多线程抢夺资源
2.  synchronized和ReentrantLock,都是独占,每次只可以一个操作,不能共享
3.  ReentrantReadWriteLock,读读可以共享,提升性能,但是不能多人写。缺点:造成死锁(一直读,不能写),读进程不能写,写进程可以读。
4.  写锁降级为读锁(一般等级写锁高于读锁)

读写锁的演变:
在这里插入图片描述
在这里插入图片描述

具体第四步演练的代码
具体降级步骤 :获取写锁->获取读锁->释放写锁->释放读锁

//演示读写锁降级
public class Demo1 {

   public static void main(String[] args) {
       //可重入读写锁对象
       ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
       ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();//读锁
       ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();//写锁

       //锁降级
       //1 获取写锁
       writeLock.lock();
       System.out.println("manongyanjiuseng");
       
       //2 获取读锁
       readLock.lock();
       System.out.println("---read");
       
       //3 释放写锁
       writeLock.unlock();

       //4 释放读锁
       readLock.unlock();
   }
}

执行结果

manongyanjiuseng
---read

如果是读之后再写,执行不了 ,因为读锁权限小于写锁 。需要读完之后释放读锁,在进行写锁。

执行下面代码会报错

//2 获取读锁
readLock.lock();
System.out.println("---read");

//1 获取写锁
writeLock.lock();
System.out.println("manongyanjiuseng");

二、阻塞队列

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

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

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

2. LinkedBlockingQueue
基于链表的阻塞队列。由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

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

4.PriorityBlockingQueue
基于优先级的阻塞队列。支持优先级排序的无界阻塞队列。不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。

5.SynchronousQueue
一种无缓冲的等待队列。相对于有缓冲的 BlockingQueue 来说,少了一个中间经销商的环节(缓冲区)。不存储元素的阻塞队列,也即单个元素的队列。

声明一个 SynchronousQueue 有两种不同的方式,它们之间有着不太一样的行为。
公平模式和非公平模式的区别:
公平模式:SynchronousQueue 会采用公平锁,并配合一个 FIFO 队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;
非公平模式(SynchronousQueue 默认):SynchronousQueue 采用非公平锁,同时配合一个 LIFO 队列来管理多余的生产者和消费者。

而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。

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

  • 预占模式。意思就是消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,生成一个节点(节点元素为 null)入队,消费者线程被等待在这个节点上,生产者线程入队时发现有一个元素为 null 的节点,生产者线程就不入队了,直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素,从调用的方法返回。

7.LinkedBlockingDeque
由链表结构组成的双向阻塞队列。
阻塞有两种情况:

  • 插入元素时: 如果当前队列已满将会进入阻塞状态,一直等到队列有空的位置时再该元素插入,该操作可以通过设置超时参数,超时后返回 false 表示操作失败,也可以不设置超时参数一直阻塞,中断后抛出 InterruptedException异常
  • 读取元素时: 如果当前队列为空会阻塞住直到队列不为空然后返回元素,同样可以通过设置超时参数
2、方法


创建阻塞队列 BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);

  • 加入元素System.out.println(blockingQueue.add("a"));,成功为true,失败为false
  • 检查元素System.out.println(blockingQueue.element());
  • 取出元素System.out.println(blockingQueue.remove());,先进先出

第二种方法:
加入元素System.out.println(blockingQueue.offer("a"));
取出元素System.out.println(blockingQueue.poll());

第三种方法:
加入元素blockingQueue.put("a");
取出元素System.out.println(blockingQueue.take());

该方法加入元素或者取出元素,如果满了或者空了,还进行下一步加入或者取出操作,会出现阻塞的状态,而第一二种方法是直接抛出异常。

第四种方法:
加入元素System.out.println(blockingQueue.offer("a"));
该方法满了或者空了在进行会有阻塞,但可以加入参数,超时退出System.out.println(blockingQueue.offer("w",3L, TimeUnit.SECONDS));

三、线程池

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

线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。

特点:

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

具体架构:
Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor 这几个类

说明:Executors为工具类,I为接口类,C为实现类

1、种类与创建
  • Executors.newFixedThreadPool(int)一池N线程

    ExecutorService threadPool1 = Executors.newFixedThreadPool(5); //5个窗口

  • Executors.newSingleThreadExecutor()一池一线程

    ExecutorService threadPool2 = Executors.newSingleThreadExecutor(); //一个窗口

  • Executors.newCachedThreadPool()一池可扩容根据需求创建线程

    ExecutorService threadPool3 = Executors.newCachedThreadPool();

执行线程execute()
关闭线程shutdown()

线程池中的执行方法execute源代码为

public interface Executor {
    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}

void execute(Runnable command);参数为Runnable接口类。

具体案例代码案例

//演示线程池三种常用分类
public class ThreadPoolDemo1 {
     public static void main(String[] args) {
         //一池五线程
         ExecutorService threadPool1 = Executors.newFixedThreadPool(5); //5个窗口
 
         //一池一线程
         ExecutorService threadPool2 = Executors.newSingleThreadExecutor(); //一个窗口
 
         //一池可扩容线程
         ExecutorService threadPool3 = Executors.newCachedThreadPool();
         //10个顾客请求
         try {
             for (int i = 1; i <=10; i++) {
                 //执行
                 threadPool3.execute(()->{
                     System.out.println(Thread.currentThread().getName()+" 办理业务");
                 });
             }
         }catch (Exception e) {
             e.printStackTrace();
         }finally {
             //关闭
             threadPool3.shutdown();
         } 
     } 
 }
2、底层原理

通过查看上面三种方式创建对象的类源代码
都有new ThreadPoolExecutor
具体查看该类的源代码,涉及七个参数

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
    BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
    if (corePoolSize < 0 || maximumPoolSize <= 0 ||
      maximumPoolSize < corePoolSize || keepAliveTime < 0)
      throw new IllegalArgumentException();
      if (workQueue == null || threadFactory == null || handler == null)
         throw new NullPointerException();
       this.corePoolSize = corePoolSize;
       this.maximumPoolSize = maximumPoolSize;
       this.workQueue = workQueue;
       this.keepAliveTime = unit.toNanos(keepAliveTime);
       this.threadFactory = threadFactory;
       this.handler = handler;
}

具体代码中的七个参数讲解:
int corePoolSize, 常驻线程数量(核心)
int maximumPoolSize,最大线程数量
long keepAliveTime,TimeUnit unit,线程存活时间
BlockingQueue<Runnable> workQueue,阻塞队列(排队的线程放入)
ThreadFactory threadFactory,线程工厂,用于创建线程
RejectedExecutionHandler handler拒绝策略

具体工作流程是:

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

阻塞队列为3,常驻线程数2,最大线程数5

具体的拒绝策略有:

  1. 抛异常
  2. 谁调用找谁
  3. 抛弃最久,执行当前
  4. 不理不问
3、自定义线程池

实际在开发中不允许使用Executors创建。
FixedThreadPoolSingleThreadExecutor:允许请求队列最大长度为Integer.MAX_VALUE,导致OOM;
CachedThreadPoolScheduledThreadPool:允许创建线程的最大数量为Integer.MAX_VALUE,导致OOM。
通过ThreadPoolExecutor的方式,规避资源耗尽风险。
示例:

ExecutorService threadPool = new ThreadPoolExecutor(2, 5, 2L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

完整代码演示

//自定义线程池创建
public class ThreadPoolDemo2 {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(2, 5, 2L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

        //10个顾客请求
        try {
            for (int i = 1; i <=10; i++) {
                //执行
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" 办理业务");
                });
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            //关闭
            threadPool.shutdown();
        }
    }
}

四、Fork与Join分支

将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果。该算法相当于递归,且是二分查找思路。

class Fibonacci extends RecursiveTask<Integer> {
	final int n;
	Fibonacci(int n) { this.n = n; }
	Integer compute() {
	  if (n <= 1)
	     return n;
	  Fibonacci f1 = new Fibonacci(n - 1);
	  f1.fork();
	  Fibonacci f2 = new Fibonacci(n - 2);
	  return f2.compute() + f1.join();
	}
}
  • ForkJoinTask:我们要使用 Fork/Join 框架,首先需要创建一个 ForkJoin 任务。该类提供了在任务中执行 fork 和 join 的机制。通常情况下我们不需要直接集成 ForkJoinTask 类,只需要继承它的子类,Fork/Join 框架提供了两个子类:
    RecursiveAction:用于没有返回结果的任务。
    RecursiveTask:用于有返回结果的任务。
  • ForkJoinPool:ForkJoinTask 需要通过 ForkJoinPool 来执行。
  • RecursiveTask: 继承后可以实现递归(自己调自己)调用的任务。

创建分支合并对象。通过该对象调用内部方法。

具体案例:1加到100,相加两个数值不能大于10
完整代码如下:

class MyTask extends RecursiveTask<Integer> {
    
    //拆分差值不能超过10,计算10以内运算
    private 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 middle = (begin+end)/2;
            //拆分左边
            MyTask task01 = new MyTask(begin,middle);
            //拆分右边
            MyTask task02 = new MyTask(middle+1,end);
            //调用方法拆分
            task01.fork();
            task02.fork();
            //合并结果
            result = task01.join()+task02.join();
        }
        return result;
    }
}

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

五、异步回调

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

类中的具体引用类以及接口:
在这里插入图片描述

CompletableFuture 实现了 Future, CompletionStage 接口,实现了 Future接口就可以兼容现有线程池框架,而 CompletionStage 接口才是异步编程的接口抽象,里面定义多种异步方法,通过这两者集合,从而打造出了强大的CompletableFuture 类。

异步调用没有返回值方法runAsync
异步调用有返回值方法supplyAsync

主线程调用 get 方法会阻塞

具体完整代码演示:

//异步调用和同步调用
public class CompletableFutureDemo {
    public static void main(String[] args) throws Exception {
        //同步调用
        CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(()->{
            System.out.println(Thread.currentThread().getName()+" : CompletableFuture1");
        });
        completableFuture1.get();

        //mq消息队列
        //异步调用
        CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName()+" : CompletableFuture2");
            //模拟异常
            int i = 10/0;
            return 1024;
        });
        completableFuture2.whenComplete((t,u)->{
            System.out.println("------t="+t);
            System.out.println("------u="+u);
        }).get();

    }
}

执行结果截图:
在这里插入图片描述

具体whenComplete的源代码为:
t为返回结果,u为异常信息

 public CompletableFuture<T> whenComplete(
    BiConsumer<? super T, ? super Throwable> action) {
    return uniWhenCompleteStage(null, action);
}
Future 与 CompletableFuture

对比这两种方法,一个为同步一个为异步

Futrue 在 Java 里面,通常用来表示一个异步任务的引用,比如我们将任务提交到线程池里面,然后我们会得到一个 Futrue,在 Future 里面有 isDone 方法来 判断任务是否处理结束,还有 get 方法可以一直阻塞直到任务结束然后获取结果,但整体来说这种方式,还是同步的,因为需要客户端不断阻塞等待或者不断轮询才能知道任务是否完成

(1)不支持手动完成
我提交了一个任务,但是执行太慢了,我通过其他路径已经获取到了任务结果,现在没法把这个任务结果通知到正在执行的线程,所以必须主动取消或者一直等待它执行完成

(2)不支持进一步的非阻塞调用
通过 Future 的 get 方法会一直阻塞到任务完成,但是想在获取任务之后执行额外的任务,因为 Future 不支持回调函数,所以无法实现这个功能
(3)不支持链式调用
对于 Future 的执行结果,我们想继续传到下一个 Future 处理使用,从而形成一个链式的 pipline 调用,这在 Future 中是没法实现的。
(4)不支持多个 Future 合并
比如我们有 10 个 Future 并行执行,我们想在所有的 Future 运行完毕之后,执行某些函数,是没法通过 Future 实现的。
(5)不支持异常处理
Future 的 API 没有任何的异常处理的 api,所以在异步运行时,如果出了问题是不好定位的。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

exodus3

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值